--[[ https://github.com/overextended/ox_lib This file is licensed under LGPL-3.0 or higher Copyright © 2025 Linden ]] ---@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 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[] ---@param parentMenuId string? If provided, the item(s) will be added to the specified radial submenu instead of the global menu. function lib.addRadialItem(items, parentMenuId) local targetMenu if parentMenuId then if not menus[parentMenuId] then return error('No radial submenu with such parent id found.') end targetMenu = menus[parentMenuId].items else targetMenu = menuItems end local menuSize = #targetMenu 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 targetMenu[menuSize] = item else for j = 1, menuSize do if targetMenu[j].id == item.id then targetMenu[j] = item break end if j == menuSize then menuSize += 1 targetMenu[menuSize] = item end end end end if isOpen and not currentRadial then refreshRadial() end end ---Removes an item from the global radial menu or a specific radial submenu with the given id. ---@param id string ---@param parentMenuId string? If provided, the item will be removed from the specified radial submenu instead of the global menu. function lib.removeRadialItem(id, parentMenuId) local menuItem local targetMenu if parentMenuId then if not menus[parentMenuId] then return error('No radial sub menu with such parent id found.') end targetMenu = menus[parentMenuId].items else targetMenu = menuItems end for i = 1, #targetMenu do menuItem = targetMenu[i] if menuItem.id == id then table.remove(targetMenu, i) break end end if not isOpen then return end refreshRadial() 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)