Radial menu
A wheel of items the player opens with a key (F1 by default) and picks from with the mouse or the number keys. tm.radial is client only. You add items to the main wheel and register submenus, and tm_lib draws them with the Twilight Moon wheel, or hands them to ox_lib or qb-radialmenu when the server owner chose one of those. Your code is the same either way.
tm.radial.addItem({
id = 'garage',
label = tm.locale.t('garage.title'),
icon = 'car',
event = 'tm_garages:client:open',
})
tm.radial.registerMenu({
id = 'garage_more',
title = tm.locale.t('garage.more'),
items = {
{ id = 'garage_park', label = tm.locale.t('garage.park'), icon = 'square-parking', serverEvent = 'tm_garages:server:park' },
{ id = 'garage_sell', label = tm.locale.t('garage.sell'), icon = 'dollar-sign', onSelect = function() sellVehicle() end },
},
})
tm.radial.addItem({ id = 'garage_menu', label = tm.locale.t('garage.more'), icon = 'ellipsis', menu = 'garage_more' })| Function | Notes |
|---|---|
tm.radial.addItem(item | item[]) | Adds to the main wheel. An id that exists is replaced and keeps its place. Returns true, or false, reason when an item is unusable (the usable ones are still added). |
tm.radial.removeItem(id) | Removes from the main wheel. Returns whether it existed. |
tm.radial.registerMenu({ id, title?, items }) | Registers a submenu. Items use the same shape and may point at deeper menus. Registering an id again replaces the menu. |
tm.radial.removeMenu(id) | Removes a submenu. Returns whether it existed. |
tm.radial.open(menuId?) | Opens the main wheel, or a registered menu (without a Back button). Returns true, or false, reason. |
tm.radial.close() | Closes the wheel. |
tm.radial.isOpen() | Whether the wheel is on screen. |
tm.radial.disable(state) | While true the wheel cannot be opened, and an open one is closed. Turned off again by false, or when the resource that turned it on stops. |
tm.radial.clear() | Removes every item and menu your resource added. |
open refuses with 'disabled', 'empty' (nothing to show), 'unknown menu' or 'unsupported' (the configured system only opens from its own key).
Items
| Field | Meaning |
|---|---|
id, label | Required. The id is unique per wheel (numbers are converted to text). |
icon | A Font Awesome name ('car', 'fas fa-car', { 'fas', 'car' }), 'item:water' or an image URL. See icons. A lucide: name only draws if the page bundles it (an unknown one shows a dashed circle), so prefer Font Awesome names here, which every wheel system understands. |
order | Lower comes first. Default 0. Items with the same order keep the order they were added in. |
onSelect | function(args) run when the item is picked. |
event, serverEvent | Client or server event triggered with args. |
command | Command executed. When args is a string or a number it is appended: command = 'e', args = 'wave' runs e wave. |
args | Passed to onSelect and both events. |
menu | Id of a registered menu. Picking the item opens it and runs no action. |
keepOpen | The wheel stays open after the item ran. |
disabled | Shown, but picking it does nothing. Add the item again to change it. |
canShow | function(): boolean, checked every time the wheel opens. false hides the item. |
onSelect, the events and the command all run when they are set, in that order. A failing one is logged and the others still run. The wheel closes before they run (unless keepOpen), so an action is free to open a dialog or a menu.
A level shows at most 24 items, the page paginates them. The rest are dropped and the level is logged once.
Cleanup
Everything a resource adds is removed when that resource stops, and so is a disable it set. tm.radial.clear() does the same on demand. Add items when the player loads or when a state changes, and remove them again, the same way you would with a target zone.
AddEventHandler('tm_lib:fw:jobChanged', function(job)
if job.name == 'police' then
tm.radial.addItem({ id = 'cuff', label = tm.locale.t('police.cuff'), icon = 'hand', event = 'police:client:cuff' })
else
tm.radial.removeItem('cuff')
end
end)Prefer canShow for things that depend on the moment (in a vehicle, near a player). It runs only when the wheel opens, so it can be as cheap or as expensive as it needs to be, but keep it short.
The key
tm_lib registers a rebindable key mapping (command tm_radial, default F1, listed as "Open the radial menu" in the GTA key settings). Pressing it opens the main wheel and pressing it again closes it, Escape or a right click closes it too. It does nothing while the wheel is disabled, empty, while the player is dead or in the pause menu, or while another UI holds the mouse. A wheel that is open is closed when the player dies or opens the pause menu.
The key only works for the built in wheel. When tm:radial points at another system, that system opens with its own key.
Which system draws it
The server owner picks it with setr tm:radial <name> (see the bridge): tm (default), ox_lib, qb-radialmenu, none or custom. Your resource never needs to know. Two things to keep in mind when you write for all of them:
- ox_lib and qb-radialmenu cannot hide an item per open.
canShowis still honored: the item is shown, and picking it does nothing whilecanShowsays no. - Only the built in wheel can be opened and closed from Lua. The others answer
tm.radial.open()withfalse, 'unsupported'.
If the configured system is not running, the built in wheel is used and everything you added is moved into it. When the system comes back, it moves back. You do not have to do anything for that.
Development notes
The Lua side is resource/interface/client/radial.lua (the model, the actions, ownership, switching adapters) and bridge/radial/tm/client.lua (the wheel). The page (ui/src/components/radial) only draws the level it is sent and reports clicks. Lua to page: @radial.open with { id, title?, back, items } and @radial.close. Page to Lua: radialSelect with { id }, radialBack and radialClose. Navigation between levels is decided in Lua.