353 lines
		
	
	
	
		
			8.2 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			353 lines
		
	
	
	
		
			8.2 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| --[[
 | |
|     https://github.com/overextended/ox_lib
 | |
| 
 | |
|     This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
 | |
| 
 | |
|     Copyright © 2025 Linden <https://github.com/thelindat>
 | |
| ]]
 | |
| 
 | |
| ---@class RadialItem
 | |
| ---@field icon string | {[1]: IconProp, [2]: string};
 | |
| ---@field label string
 | |
| ---@field menu? string
 | |
| ---@field onSelect? fun(currentMenu: string | nil, itemIndex: number) | string
 | |
| ---@field [string] any
 | |
| ---@field keepOpen? boolean
 | |
| ---@field iconWidth? number
 | |
| ---@field iconHeight? number
 | |
| 
 | |
| ---@class RadialMenuItem: RadialItem
 | |
| ---@field id string
 | |
| 
 | |
| ---@class RadialMenuProps
 | |
| ---@field id string
 | |
| ---@field items RadialItem[]
 | |
| ---@field [string] any
 | |
| 
 | |
| local isOpen = false
 | |
| 
 | |
| ---@type table<string, RadialMenuProps>
 | |
| local menus = {}
 | |
| 
 | |
| ---@type RadialMenuItem[]
 | |
| local menuItems = {}
 | |
| 
 | |
| ---@type table<{id: string, option: string}>
 | |
| local menuHistory = {}
 | |
| 
 | |
| ---@type RadialMenuProps?
 | |
| local currentRadial = nil
 | |
| 
 | |
| ---Open a the global radial menu or a registered radial submenu with the given id.
 | |
| ---@param id string?
 | |
| ---@param option number?
 | |
| local function showRadial(id, option)
 | |
|     local radial = id and menus[id]
 | |
| 
 | |
|     if id and not radial then
 | |
|         return error('No radial menu with such id found.')
 | |
|     end
 | |
| 
 | |
|     currentRadial = radial
 | |
| 
 | |
|     -- Hide current menu and allow for transition
 | |
|     SendNUIMessage({
 | |
|         action = 'openRadialMenu',
 | |
|         data = false
 | |
|     })
 | |
| 
 | |
|     Wait(100)
 | |
| 
 | |
|     -- If menu was closed during transition, don't open the submenu
 | |
|     if not isOpen then return end
 | |
| 
 | |
|     SendNUIMessage({
 | |
|         action = 'openRadialMenu',
 | |
|         data = {
 | |
|             items = radial and radial.items or menuItems,
 | |
|             sub = radial and true or nil,
 | |
|             option = option
 | |
|         }
 | |
|     })
 | |
| end
 | |
| 
 | |
| ---Refresh the current menu items or return from a submenu to its parent.
 | |
| local function refreshRadial(menuId)
 | |
|     if not isOpen then return end
 | |
| 
 | |
|     if currentRadial and menuId then
 | |
|         if menuId == currentRadial.id then
 | |
|             return showRadial(menuId)
 | |
|         else
 | |
|             for i = 1, #menuHistory do
 | |
|                 local subMenu = menuHistory[i]
 | |
| 
 | |
|                 if subMenu.id == menuId then
 | |
|                     local parent = menus[subMenu.id]
 | |
| 
 | |
|                     for j = 1, #parent.items do
 | |
|                         -- If we still have a path to the current submenu, refresh instead of returning
 | |
|                         if parent.items[j].menu == currentRadial.id then
 | |
|                             return -- showRadial(currentRadial.id)
 | |
|                         end
 | |
|                     end
 | |
| 
 | |
|                     currentRadial = parent
 | |
| 
 | |
|                     for j = #menuHistory, i, -1 do
 | |
|                         menuHistory[j] = nil
 | |
|                     end
 | |
| 
 | |
|                     return showRadial(currentRadial.id)
 | |
|                 end
 | |
|             end
 | |
|         end
 | |
| 
 | |
|         return
 | |
|     end
 | |
| 
 | |
|     table.wipe(menuHistory)
 | |
|     showRadial()
 | |
| end
 | |
| 
 | |
| ---Registers a radial sub menu with predefined options.
 | |
| ---@param radial RadialMenuProps
 | |
| function lib.registerRadial(radial)
 | |
|     menus[radial.id] = radial
 | |
|     radial.resource = GetInvokingResource()
 | |
| 
 | |
|     if currentRadial then
 | |
|         refreshRadial(radial.id)
 | |
|     end
 | |
| end
 | |
| 
 | |
| function lib.getCurrentRadialId()
 | |
|     return currentRadial and currentRadial.id
 | |
| end
 | |
| 
 | |
| function lib.hideRadial()
 | |
|     if not isOpen then return end
 | |
| 
 | |
|     SendNUIMessage({
 | |
|         action = 'openRadialMenu',
 | |
|         data = false
 | |
|     })
 | |
| 
 | |
|     lib.resetNuiFocus()
 | |
|     table.wipe(menuHistory)
 | |
| 
 | |
|     isOpen = false
 | |
|     currentRadial = nil
 | |
| end
 | |
| 
 | |
| ---Registers an item or array of items in the global radial menu.
 | |
| ---@param items RadialMenuItem | RadialMenuItem[]
 | |
| function lib.addRadialItem(items)
 | |
|     local menuSize = #menuItems
 | |
|     local invokingResource = GetInvokingResource()
 | |
| 
 | |
|     items = table.type(items) == 'array' and items or { items }
 | |
| 
 | |
|     for i = 1, #items do
 | |
|         local item = items[i]
 | |
|         item.resource = invokingResource
 | |
| 
 | |
|         if menuSize == 0 then
 | |
|             menuSize += 1
 | |
|             menuItems[menuSize] = item
 | |
|         else
 | |
|             for j = 1, menuSize do
 | |
|                 if menuItems[j].id == item.id then
 | |
|                     menuItems[j] = item
 | |
|                     break
 | |
|                 end
 | |
| 
 | |
|                 if j == menuSize then
 | |
|                     menuSize += 1
 | |
|                     menuItems[menuSize] = item
 | |
|                 end
 | |
|             end
 | |
|         end
 | |
|     end
 | |
| 
 | |
|     if isOpen and not currentRadial then
 | |
|         refreshRadial()
 | |
|     end
 | |
| end
 | |
| 
 | |
| ---Removes an item from the global radial menu with the given id.
 | |
| ---@param id string
 | |
| function lib.removeRadialItem(id)
 | |
|     local menuItem
 | |
| 
 | |
|     for i = 1, #menuItems do
 | |
|         menuItem = menuItems[i]
 | |
| 
 | |
|         if menuItem.id == id then
 | |
|             table.remove(menuItems, i)
 | |
|             break
 | |
|         end
 | |
|     end
 | |
| 
 | |
|     if not isOpen then return end
 | |
| 
 | |
|     refreshRadial(id)
 | |
| end
 | |
| 
 | |
| ---Removes all items from the global radial menu.
 | |
| function lib.clearRadialItems()
 | |
|     table.wipe(menuItems)
 | |
| 
 | |
|     if isOpen then
 | |
|         refreshRadial()
 | |
|     end
 | |
| end
 | |
| 
 | |
| RegisterNUICallback('radialClick', function(index, cb)
 | |
|     cb(1)
 | |
| 
 | |
|     local itemIndex = index + 1
 | |
|     local item, currentMenu
 | |
| 
 | |
|     if currentRadial then
 | |
|         item = currentRadial.items[itemIndex]
 | |
|         currentMenu = currentRadial.id
 | |
|     else
 | |
|         item = menuItems[itemIndex]
 | |
|     end
 | |
| 
 | |
|     local menuResource = currentRadial and currentRadial.resource or item.resource
 | |
| 
 | |
|     if item.menu then
 | |
|         menuHistory[#menuHistory + 1] = { id = currentRadial and currentRadial.id, option = item.menu }
 | |
|         showRadial(item.menu)
 | |
|     elseif not item.keepOpen then
 | |
|         lib.hideRadial()
 | |
|     end
 | |
| 
 | |
|     local onSelect = item.onSelect
 | |
| 
 | |
|     if onSelect then
 | |
|         if type(onSelect) == 'string' then
 | |
|             return exports[menuResource][onSelect](0, currentMenu, itemIndex)
 | |
|         end
 | |
| 
 | |
|         onSelect(currentMenu, itemIndex)
 | |
|     end
 | |
| end)
 | |
| 
 | |
| RegisterNUICallback('radialBack', function(_, cb)
 | |
|     cb(1)
 | |
| 
 | |
|     local numHistory = #menuHistory
 | |
|     local lastMenu = numHistory > 0 and menuHistory[numHistory]
 | |
| 
 | |
|     if not lastMenu then return end
 | |
| 
 | |
|     menuHistory[numHistory] = nil
 | |
| 
 | |
|     if lastMenu.id then
 | |
|         return showRadial(lastMenu.id, lastMenu.option)
 | |
|     end
 | |
| 
 | |
|     currentRadial = nil
 | |
| 
 | |
|     -- Hide current menu and allow for transition
 | |
|     SendNUIMessage({
 | |
|         action = 'openRadialMenu',
 | |
|         data = false
 | |
|     })
 | |
| 
 | |
|     Wait(100)
 | |
| 
 | |
|     -- If menu was closed during transition, don't open the submenu
 | |
|     if not isOpen then return end
 | |
| 
 | |
|     SendNUIMessage({
 | |
|         action = 'openRadialMenu',
 | |
|         data = {
 | |
|             items = menuItems,
 | |
|             option = lastMenu.option
 | |
|         }
 | |
|     })
 | |
| end)
 | |
| 
 | |
| RegisterNUICallback('radialClose', function(_, cb)
 | |
|     cb(1)
 | |
| 
 | |
|     if not isOpen then return end
 | |
| 
 | |
|     lib.resetNuiFocus()
 | |
| 
 | |
|     isOpen = false
 | |
|     currentRadial = nil
 | |
| end)
 | |
| 
 | |
| RegisterNUICallback('radialTransition', function(_, cb)
 | |
|     Wait(100)
 | |
| 
 | |
|     -- If menu was closed during transition, don't open the submenu
 | |
|     if not isOpen then return cb(false) end
 | |
| 
 | |
|     cb(true)
 | |
| end)
 | |
| 
 | |
| local isDisabled = false
 | |
| 
 | |
| ---Disallow players from opening the radial menu.
 | |
| ---@param state boolean
 | |
| function lib.disableRadial(state)
 | |
|     isDisabled = state
 | |
| 
 | |
|     if isOpen and state then
 | |
|         return lib.hideRadial()
 | |
|     end
 | |
| end
 | |
| 
 | |
| lib.addKeybind({
 | |
|     name = 'ox_lib-radial',
 | |
|     description = locale('open_radial_menu'),
 | |
|     defaultKey = 'z',
 | |
|     onPressed = function()
 | |
|         if isDisabled then return end
 | |
| 
 | |
|         if isOpen then
 | |
|             return lib.hideRadial()
 | |
|         end
 | |
| 
 | |
|         if #menuItems == 0 or IsNuiFocused() or IsPauseMenuActive() then return end
 | |
| 
 | |
|         isOpen = true
 | |
| 
 | |
|         SendNUIMessage({
 | |
|             action = 'openRadialMenu',
 | |
|             data = {
 | |
|                 items = menuItems
 | |
|             }
 | |
|         })
 | |
| 
 | |
|         lib.setNuiFocus(true)
 | |
|         SetCursorLocation(0.5, 0.5)
 | |
| 
 | |
|         while isOpen do
 | |
|             DisablePlayerFiring(cache.playerId, true)
 | |
|             DisableControlAction(0, 1, true)
 | |
|             DisableControlAction(0, 2, true)
 | |
|             DisableControlAction(0, 142, true)
 | |
|             DisableControlAction(2, 199, true)
 | |
|             DisableControlAction(2, 200, true)
 | |
|             Wait(0)
 | |
|         end
 | |
|     end,
 | |
|     -- onReleased = lib.hideRadial,
 | |
| })
 | |
| 
 | |
| AddEventHandler('onClientResourceStop', function(resource)
 | |
|     for i = #menuItems, 1, -1 do
 | |
|         local item = menuItems[i]
 | |
| 
 | |
|         if item.resource == resource then
 | |
|             table.remove(menuItems, i)
 | |
|         end
 | |
|     end
 | |
| end)
 | 
