├── README.md ├── fxmanifest.lua ├── .github └── workflows │ └── lockdown.yml ├── src ├── components │ ├── Util.lua │ ├── Keys.lua │ ├── Audio.lua │ ├── Visual.lua │ └── Graphics.lua └── ContextUI.lua ├── example.lua └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | # ContextUI 2 | 3 | ContextUI is an interface of actions usable on entities. 4 | 5 | Example -> https://gyazo.com/4ff028f3ccb257b35bb77334e2691333 6 | 7 | Keep informed on Riv-Tech https://discord.gg/TKX2nVq9ue 8 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- @author Dylan MALANDAIN, Kalyptus 3 | --- @version 1.0.0 4 | --- created at [26/05/2021 10:22] 5 | --- 6 | 7 | fx_version 'cerulean' 8 | 9 | games { 'gta5' }; 10 | 11 | client_scripts { 12 | "src/components/*.lua", 13 | "src/ContextUI.lua", 14 | "example.lua" 15 | } 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/lockdown.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock down repository' 2 | 3 | on: 4 | issues: 5 | types: opened 6 | pull_request: 7 | types: opened 8 | schedule: 9 | - cron: '0 * * * *' 10 | 11 | jobs: 12 | lockdown: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: dessant/repo-lockdown@v2 16 | with: 17 | github-token: ${{ github.token }} -------------------------------------------------------------------------------- /src/components/Util.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- @author Dylan MALANDAIN, Kalyptus 3 | --- @version 1.0.0 4 | --- File created at [24/05/2021 09:57] 5 | --- 6 | 7 | function math.round(num, numDecimalPlaces) 8 | return tonumber(string.format("%." .. (numDecimalPlaces or 0) .. "f", num)) 9 | end 10 | 11 | function string.starts(String, Start) 12 | return string.sub(String, 1, string.len(Start)) == Start 13 | end -------------------------------------------------------------------------------- /src/components/Keys.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- @author Dylan MALANDAIN, Kalyptus 3 | --- @version 1.0.0 4 | --- File created at [24/05/2021 00:00] 5 | --- 6 | 7 | Keys = {}; 8 | 9 | ---Register 10 | ---@param Controls string 11 | ---@param ControlName string 12 | ---@param Description string 13 | ---@param Action function 14 | ---@return Keys 15 | ---@public 16 | function Keys.Register(Controls, ControlName, Description, Action) 17 | RegisterKeyMapping(string.format('keys-%s', ControlName), Description, "keyboard", Controls) 18 | RegisterCommand(string.format('keys-%s', ControlName), function() 19 | if (Action ~= nil) then 20 | Action(); 21 | end 22 | end, false) 23 | end -------------------------------------------------------------------------------- /src/components/Audio.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- @author Dylan MALANDAIN, Kalyptus 3 | --- @version 1.0.0 4 | --- File created at [24/05/2021 00:00] 5 | --- 6 | 7 | Audio = {} 8 | 9 | ---PlaySound 10 | --- 11 | --- Reference : N/A 12 | --- 13 | ---@param Library string 14 | ---@param Sound string 15 | ---@param IsLooped boolean 16 | ---@return nil 17 | ---@public 18 | function Audio.PlaySound(Library, Sound, IsLooped) 19 | local audioId 20 | if not IsLooped then 21 | PlaySoundFrontend(-1, Sound, Library, true) 22 | else 23 | if not audioId then 24 | Citizen.CreateThread(function() 25 | audioId = GetSoundId() 26 | PlaySoundFrontend(audioId, Sound, Library, true) 27 | Citizen.Wait(0.01) 28 | StopSound(audioId) 29 | ReleaseSoundId(audioId) 30 | audioId = nil 31 | end) 32 | end 33 | end 34 | end 35 | 36 | 37 | -------------------------------------------------------------------------------- /example.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- @author Dylan MALANDAIN, Kalyptus 3 | --- @version 1.0.0 4 | --- File created at [26/05/2021 10:28] 5 | --- 6 | 7 | --EntityType 1 = Ped 8 | --EntityType 2 = Vehicle 9 | --EntityType 3 = Object 10 | 11 | local main = ContextUI:CreateMenu(1, "Example title") 12 | local submenu = ContextUI:CreateSubMenu(main) 13 | 14 | ContextUI:IsVisible(main, function(Entity) 15 | for i=1, 10 do 16 | ContextUI:Button("Button #"..i, nil, function(onSelected) 17 | if (onSelected) then 18 | print("onSelected #"..i) 19 | submenu.Title = "Button #"..i 20 | end 21 | end, submenu) 22 | end 23 | end) 24 | 25 | ContextUI:IsVisible(submenu, function(Entity) 26 | for k, v in pairs(Entity) do 27 | ContextUI:Button(k, v, function(onSelected) 28 | if onSelected then 29 | print(v) 30 | end 31 | end) 32 | end 33 | end) 34 | 35 | --LMENU => LEFT ALT 36 | Keys.Register("LMENU", "LMENU", "Enable / disable focus mode.", function() 37 | ContextUI.Focus = not ContextUI.Focus; 38 | end) -------------------------------------------------------------------------------- /src/components/Visual.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- @author Dylan MALANDAIN, Kalyptus 3 | --- @version 1.0.0 4 | --- File created at [24/05/2021 00:00] 5 | --- 6 | 7 | Visual = {}; 8 | 9 | local function AddLongString(txt) 10 | for i = 100, string.len(txt), 99 do 11 | local sub = string.sub(txt, i, i + 99) 12 | AddTextComponentSubstringPlayerName(sub) 13 | end 14 | end 15 | 16 | function Visual.Notification(args) 17 | if (not args.dict) and (args.name )then 18 | args.dict = args.name 19 | end 20 | if not HasStreamedTextureDictLoaded(args.dict) then 21 | RequestStreamedTextureDict(args.dict, false) 22 | while not HasStreamedTextureDictLoaded(args.dict) do Wait(0) end 23 | end 24 | if (args.backId) then 25 | ThefeedNextPostBackgroundColor(args.backId) 26 | end 27 | BeginTextCommandThefeedPost("jamyfafi") 28 | if (args.message) then 29 | AddTextComponentSubstringPlayerName(args.message) 30 | if string.len(args.message) > 99 then 31 | AddLongString(args.message) 32 | end 33 | end 34 | if (args.title) and (args.subtitle) and (args.name) then 35 | EndTextCommandThefeedPostMessagetext(args.dict or "CHAR_DEFAULT", args.name or "CHAR_DEFAULT", true, args.icon or 0, args.title or "", args.subtitle or "") 36 | SetStreamedTextureDictAsNoLongerNeeded(args.dict) 37 | end 38 | EndTextCommandThefeedPostTicker(false, true) 39 | end 40 | 41 | function Visual.Subtitle(text, time) 42 | ClearPrints() 43 | BeginTextCommandPrint("STRING") 44 | AddTextComponentSubstringPlayerName(text) 45 | EndTextCommandPrint(time and math.ceil(time) or 0, true) 46 | end 47 | 48 | function Visual.FloatingHelpText(text, sound, loop) 49 | BeginTextCommandDisplayHelp("jamyfafi") 50 | AddTextComponentSubstringPlayerName(text) 51 | if string.len(text) > 99 then 52 | AddLongString(text) 53 | end 54 | EndTextCommandDisplayHelp(0, loop or 0, sound or true, -1) 55 | end 56 | 57 | function Visual.Prompt(text, spinner) 58 | BeginTextCommandBusyspinnerOn("STRING") 59 | AddTextComponentSubstringPlayerName(text) 60 | EndTextCommandBusyspinnerOn(spinner or 1) 61 | end 62 | 63 | function Visual.PromptDuration(duration, text, spinner) 64 | Citizen.CreateThread(function() 65 | Citizen.Wait(0) 66 | Visual.Prompt(text, spinner) 67 | Citizen.Wait(duration) 68 | if (BusyspinnerIsOn()) then 69 | BusyspinnerOff(); 70 | end 71 | end) 72 | end 73 | 74 | function Visual.FloatingHelpTextToEntity(text, x, y) 75 | SetFloatingHelpTextScreenPosition(1, x, y) 76 | SetFloatingHelpTextStyle(1, 1, 2, -1, 3, 0) 77 | BeginTextCommandDisplayHelp("jamyfafi") 78 | AddTextComponentSubstringPlayerName(text) 79 | if string.len(text) > 99 then 80 | AddLongString(text) 81 | end 82 | EndTextCommandDisplayHelp(2, false, false, -1) 83 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/lua,jetbrains 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=lua,jetbrains 4 | 5 | ### JetBrains ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | /.idea/ 16 | 17 | 18 | # Generated files 19 | .idea/**/contentModel.xml 20 | 21 | # Sensitive or high-churn files 22 | .idea/**/dataSources/ 23 | .idea/**/dataSources.ids 24 | .idea/**/dataSources.local.xml 25 | .idea/**/sqlDataSources.xml 26 | .idea/**/dynamic.xml 27 | .idea/**/uiDesigner.xml 28 | .idea/**/dbnavigator.xml 29 | angular/dist 30 | # Gradle 31 | .idea/**/gradle.xml 32 | .idea/**/libraries 33 | 34 | # Gradle and Maven with auto-import 35 | # When using Gradle or Maven with auto-import, you should exclude module files, 36 | # since they will be recreated, and may cause churn. Uncomment if using 37 | # auto-import. 38 | # .idea/artifacts 39 | # .idea/compiler.xml 40 | # .idea/jarRepositories.xml 41 | # .idea/modules.xml 42 | # .idea/*.iml 43 | # .idea/modules 44 | # *.iml 45 | # *.ipr 46 | 47 | # CMake 48 | cmake-build-*/ 49 | 50 | # Mongo Explorer plugin 51 | .idea/**/mongoSettings.xml 52 | 53 | # File-based project format 54 | *.iws 55 | 56 | # IntelliJ 57 | out/ 58 | 59 | # mpeltonen/sbt-idea plugin 60 | .idea_modules/ 61 | 62 | # JIRA plugin 63 | atlassian-ide-plugin.xml 64 | 65 | # Cursive Clojure plugin 66 | .idea/replstate.xml 67 | 68 | # Crashlytics plugin (for Android Studio and IntelliJ) 69 | com_crashlytics_export_strings.xml 70 | crashlytics.properties 71 | crashlytics-build.properties 72 | fabric.properties 73 | 74 | # Editor-based Rest Client 75 | .idea/httpRequests 76 | 77 | # Android studio 3.1+ serialized cache file 78 | .idea/caches/build_file_checksums.ser 79 | 80 | ### JetBrains Patch ### 81 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 82 | 83 | # *.iml 84 | # modules.xml 85 | # .idea/misc.xml 86 | # *.ipr 87 | 88 | # Sonarlint plugin 89 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 90 | .idea/**/sonarlint/ 91 | 92 | # SonarQube Plugin 93 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 94 | .idea/**/sonarIssues.xml 95 | 96 | # Markdown Navigator plugin 97 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 98 | .idea/**/markdown-navigator.xml 99 | .idea/**/markdown-navigator-enh.xml 100 | .idea/**/markdown-navigator/ 101 | 102 | # Cache file creation bug 103 | # See https://youtrack.jetbrains.com/issue/JBR-2257 104 | .idea/$CACHE_FILE$ 105 | 106 | # CodeStream plugin 107 | # https://plugins.jetbrains.com/plugin/12206-codestream 108 | .idea/codestream.xml 109 | 110 | ### Lua ### 111 | # Compiled Lua sources 112 | luac.out 113 | 114 | # luarocks build files 115 | *.src.rock 116 | *.zip 117 | *.tar.gz 118 | 119 | # Object files 120 | *.o 121 | *.os 122 | *.ko 123 | *.obj 124 | *.elf 125 | 126 | # Precompiled Headers 127 | *.gch 128 | *.pch 129 | 130 | # Libraries 131 | *.lib 132 | *.a 133 | *.la 134 | *.lo 135 | *.def 136 | *.exp 137 | 138 | # Shared objects (inc. Windows DLLs) 139 | *.dll 140 | *.so 141 | *.so.* 142 | *.dylib 143 | 144 | # Executables 145 | *.exe 146 | *.out 147 | *.app 148 | *.i*86 149 | *.x86_64 150 | *.hex 151 | 152 | 153 | # End of https://www.toptal.com/developers/gitignore/api/lua,jetbrains 154 | /.yarn.installed 155 | /yarn.lock 156 | /node_modules/ 157 | -------------------------------------------------------------------------------- /src/components/Graphics.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- @author Dylan MALANDAIN, Kalyptus 3 | --- @version 1.0.0 4 | --- File created at [24/05/2021 00:00] 5 | --- 6 | 7 | local function StringToArray(str) 8 | local charCount = #str 9 | local strCount = math.ceil(charCount / 99) 10 | local strings = {} 11 | 12 | for i = 1, strCount do 13 | local start = (i - 1) * 99 + 1 14 | local clamp = math.clamp(#string.sub(str, start), 0, 99) 15 | local finish = ((i ~= 1) and (start - 1) or 0) + clamp 16 | 17 | strings[i] = string.sub(str, start, finish) 18 | end 19 | 20 | return strings 21 | end 22 | 23 | local function AddText(str) 24 | local str = tostring(str) 25 | local charCount = #str 26 | 27 | if charCount < 100 then 28 | AddTextComponentSubstringPlayerName(str) 29 | else 30 | local strings = StringToArray(str) 31 | for s = 1, #strings do 32 | AddTextComponentSubstringPlayerName(strings[s]) 33 | end 34 | end 35 | end 36 | 37 | local function RText(text, x, y, font, scale, r, g, b, a, alignment, dropShadow, outline, wordWrap) 38 | local Text, X, Y = text, (x or 0) / 1920, (y or 0) / 1080 39 | SetTextFont(font or 0) 40 | SetTextScale(1.0, scale or 0) 41 | SetTextColour(r or 255, g or 255, b or 255, a or 255) 42 | if dropShadow then 43 | SetTextDropShadow() 44 | end 45 | if outline then 46 | SetTextOutline() 47 | end 48 | if alignment ~= nil then 49 | if alignment == 1 or alignment == "Center" or alignment == "Centre" then 50 | SetTextCentre(true) 51 | elseif alignment == 2 or alignment == "Right" then 52 | SetTextRightJustify(true) 53 | end 54 | end 55 | if wordWrap and wordWrap ~= 0 then 56 | if alignment == 1 or alignment == "Center" or alignment == "Centre" then 57 | SetTextWrap(X - ((wordWrap / 1920) / 2), X + ((wordWrap / 1920) / 2)) 58 | elseif alignment == 2 or alignment == "Right" then 59 | SetTextWrap(0, X) 60 | else 61 | SetTextWrap(X, X + (wordWrap / 1920)) 62 | end 63 | else 64 | if alignment == 2 or alignment == "Right" then 65 | SetTextWrap(0, X) 66 | end 67 | end 68 | return Text, X, Y 69 | end 70 | 71 | Graphics = {}; 72 | 73 | function Graphics.MeasureStringWidth(str, font, scale) 74 | BeginTextCommandGetWidth("CELL_EMAIL_BCON") 75 | AddTextComponentSubstringPlayerName(str) 76 | SetTextFont(font or 0) 77 | SetTextScale(1.0, scale or 0) 78 | return EndTextCommandGetWidth(true) * 1920 79 | end 80 | 81 | function Graphics.Rectangle(x, y, width, height, r, g, b, a) 82 | local X, Y, Width, Height = (x or 0) / 1920, (y or 0) / 1080, (width or 0) / 1920, (height or 0) / 1080 83 | DrawRect(X + Width * 0.5, Y + Height * 0.5, Width, Height, r or 255, g or 255, b or 255, a or 255) 84 | end 85 | 86 | function Graphics.Sprite(dictionary, name, x, y, width, height, heading, r, g, b, a) 87 | local X, Y, Width, Height = (x or 0) / 1920, (y or 0) / 1080, (width or 0) / 1920, (height or 0) / 1080 88 | 89 | if not HasStreamedTextureDictLoaded(dictionary) then 90 | RequestStreamedTextureDict(dictionary, true) 91 | end 92 | 93 | DrawSprite(dictionary, name, X + Width * 0.5, Y + Height * 0.5, Width, Height, heading or 0, r or 255, g or 255, b or 255, a or 255) 94 | end 95 | 96 | function Graphics.GetLineCount(text, x, y, font, scale, r, g, b, a, alignment, dropShadow, outline, wordWrap) 97 | local Text, X, Y = RText(text, x, y, font, scale, r, g, b, a, alignment, dropShadow, outline, wordWrap) 98 | BeginTextCommandLineCount("CELL_EMAIL_BCON") 99 | AddText(Text) 100 | return EndTextCommandLineCount(X, Y) 101 | end 102 | 103 | function Graphics.Text(text, x, y, font, scale, r, g, b, a, alignment, dropShadow, outline, wordWrap) 104 | local Text, X, Y = RText(text, x, y, font, scale, r, g, b, a, alignment, dropShadow, outline, wordWrap) 105 | BeginTextCommandDisplayText("CELL_EMAIL_BCON") 106 | AddText(Text) 107 | EndTextCommandDisplayText(X, Y) 108 | end 109 | 110 | function Graphics.IsMouseInBounds(X, Y, Width, Height) 111 | local MX, MY = math.round(GetControlNormal(2, 239) * 1920) / 1920, math.round(GetControlNormal(2, 240) * 1080) / 1080 112 | X, Y = X / 1920, Y / 1080 113 | Width, Height = Width / 1920, Height / 1080 114 | return (MX >= X and MX <= X + Width) and (MY > Y and MY < Y + Height) 115 | end 116 | 117 | function Graphics.ConvertToPixel(x, y) 118 | return (x * 1920), (y * 1080) 119 | end 120 | 121 | function Graphics.ScreenToWorld(distance, flags) 122 | local camRot = GetGameplayCamRot(0) 123 | local camPos = GetGameplayCamCoord() 124 | local mouse = vector2(GetControlNormal(2, 239), GetControlNormal(2, 240)) 125 | local cam3DPos, forwardDir = Graphics.ScreenRelToWorld(camPos, camRot, mouse) 126 | local direction = camPos + forwardDir * distance 127 | local rayHandle = StartExpensiveSynchronousShapeTestLosProbe(cam3DPos, direction, flags, 0, 0) 128 | local _, hit, endCoords, surfaceNormal, entityHit = GetShapeTestResult(rayHandle) 129 | return (hit == 1 and true or false), endCoords, surfaceNormal, entityHit, (entityHit >= 1 and GetEntityType(entityHit) or 0), direction, mouse 130 | end 131 | 132 | function Graphics.ScreenRelToWorld(camPos, camRot, cursor) 133 | local camForward = Graphics.RotationToDirection(camRot) 134 | local rotUp = vector3(camRot.x + 1.0, camRot.y, camRot.z) 135 | local rotDown = vector3(camRot.x - 1.0, camRot.y, camRot.z) 136 | local rotLeft = vector3(camRot.x, camRot.y, camRot.z - 1.0) 137 | local rotRight = vector3(camRot.x, camRot.y, camRot.z + 1.0) 138 | local camRight = Graphics.RotationToDirection(rotRight) - Graphics.RotationToDirection(rotLeft) 139 | local camUp = Graphics.RotationToDirection(rotUp) - Graphics.RotationToDirection(rotDown) 140 | local rollRad = -(camRot.y * math.pi / 180.0) 141 | local camRightRoll = camRight * math.cos(rollRad) - camUp * math.sin(rollRad) 142 | local camUpRoll = camRight * math.sin(rollRad) + camUp * math.cos(rollRad) 143 | local point3DZero = camPos + camForward * 1.0 144 | local point3D = point3DZero + camRightRoll + camUpRoll 145 | local point2D = Graphics.World3DToScreen2D(point3D) 146 | local point2DZero = Graphics.World3DToScreen2D(point3DZero) 147 | local scaleX = (cursor.x - point2DZero.x) / (point2D.x - point2DZero.x) 148 | local scaleY = (cursor.y - point2DZero.y) / (point2D.y - point2DZero.y) 149 | local point3Dret = point3DZero + camRightRoll * scaleX + camUpRoll * scaleY 150 | local forwardDir = camForward + camRightRoll * scaleX + camUpRoll * scaleY 151 | return point3Dret, forwardDir 152 | end 153 | 154 | function Graphics.RotationToDirection(rotation) 155 | local x, z = (rotation.x * math.pi / 180.0), (rotation.z * math.pi / 180.0) 156 | local num = math.abs(math.cos(x)) 157 | return vector3((-math.sin(z) * num), (math.cos(z) * num), math.sin(x)) 158 | end 159 | 160 | function Graphics.World3DToScreen2D(pos) 161 | local _, sX, sY = GetScreenCoordFromWorldCoord(pos.x, pos.y, pos.z) 162 | return vector2(sX, sY) 163 | end -------------------------------------------------------------------------------- /src/ContextUI.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- @author Dylan MALANDAIN, Kalyptus 3 | --- @version 1.0.0 4 | --- File created at [26/05/2021 10:22] 5 | --- 6 | 7 | local Settings = { 8 | Button = { 9 | Width = 220, 10 | Height = 32, 11 | Background = { 12 | { 0, 0, 0, 180 }, 13 | { 240, 240, 240, 255 } 14 | } 15 | }, 16 | Text = { 17 | Colors = { 18 | { 255, 255, 255, 255 }, 19 | { 5, 5, 5, 255 } 20 | }, 21 | X = 8.0, 22 | Y = 4.5, 23 | Scale = 0.26, 24 | Font = 0, 25 | Center = 0, 26 | Outline = false, 27 | DropShadow = false, 28 | }, 29 | Title = { 30 | Background = { 25, 80, 150, 240 }, 31 | Text = { 255, 255, 255, 255 } 32 | } 33 | } 34 | 35 | ContextUI = { 36 | Entity = { 37 | ID = nil, 38 | Type = nil, 39 | Model = nil, 40 | NetID = nil, 41 | ServerID = nil, 42 | }, 43 | Menus = {}, 44 | Focus = false, 45 | Open = false, 46 | Position = vector2(0.0, 0.0), 47 | Offset = vector2(0.0, 0.0), 48 | Options = 0, 49 | Category = "main", 50 | CategoryID = 0, 51 | Description = nil, 52 | } 53 | 54 | function ContextUI:OnClosed() 55 | ResetEntityAlpha(self.Entity.ID) 56 | self.Entity.ID = nil 57 | self.Open = false 58 | self.Focus = false 59 | self.Category = "main" 60 | self.Options = 0 61 | end 62 | 63 | local function ShowTitle(Label) 64 | local PosX, PosY = ContextUI.Position.x, ContextUI.Position.y 65 | PosY = PosY + (ContextUI.Options * Settings.Button.Height) 66 | Graphics.Rectangle(PosX, PosY, Settings.Button.Width, Settings.Button.Height, Settings.Title.Background[1], Settings.Title.Background[2], Settings.Title.Background[3], Settings.Title.Background[4]) 67 | Graphics.Text(Label, PosX + 110, PosY + 4.0, Settings.Text.Font, 0.28, Settings.Title.Text[1], Settings.Title.Text[2], Settings.Title.Text[3], Settings.Title.Text[4], 1, false, false) 68 | ContextUI.Options = ContextUI.Options + 1 69 | ContextUI.Offset = vector2(PosX, PosY) 70 | end 71 | 72 | local function ShowDescription(Description) 73 | local PosX, PosY = ContextUI.Position.x, ContextUI.Position.y 74 | PosY = PosY + (ContextUI.Options * Settings.Button.Height) 75 | local GetLineCount = Graphics.GetLineCount(Description, PosX + 110, PosY, Settings.Text.Font, 0.24, Settings.Title.Text[1], Settings.Title.Text[2], Settings.Title.Text[3], Settings.Title.Text[4], 1, false, false, 215) 76 | Graphics.Rectangle(PosX, PosY, Settings.Button.Width, 2, Settings.Title.Background[1], Settings.Title.Background[2], Settings.Title.Background[3], Settings.Title.Background[4]) 77 | Graphics.Rectangle(PosX, PosY + 2, Settings.Button.Width, 1 + (GetLineCount * 17.5), 0, 0, 0, 160) 78 | Graphics.Text(Description, PosX + 110, PosY, Settings.Text.Font, 0.24, Settings.Title.Text[1], Settings.Title.Text[2], Settings.Title.Text[3], Settings.Title.Text[4], 1, false, false, 215) 79 | ContextUI.Offset = vector2(PosX, PosY + 3 +(GetLineCount * 17.5)) 80 | end 81 | 82 | function ContextUI:Button(Label, Description, Actions, Submenu) 83 | local PosX, PosY = self.Position.x, self.Position.y 84 | PosY = PosY + (self.Options * Settings.Button.Height) 85 | local onHovered = Graphics.IsMouseInBounds(PosX, PosY, Settings.Button.Width, Settings.Button.Height) 86 | 87 | if (onHovered) then 88 | local Selected = false; 89 | SetMouseCursorSprite(5) 90 | if IsControlJustPressed(0, 24) then 91 | Selected = true 92 | if (Submenu) then 93 | self.Category = Submenu.Category 94 | end 95 | local audioName = Label == "← Retour" and "BACK" or "SELECT" 96 | Audio.PlaySound("HUD_FRONTEND_DEFAULT_SOUNDSET", audioName, false) 97 | end 98 | if (Actions) then 99 | Actions(Selected) 100 | end 101 | self.Description = Description 102 | end 103 | 104 | local Index = (not onHovered) and 1 or 2 105 | Graphics.Rectangle(PosX, PosY, Settings.Button.Width, Settings.Button.Height, Settings.Button.Background[Index][1], Settings.Button.Background[Index][2], Settings.Button.Background[Index][3], Settings.Button.Background[Index][4]) 106 | Graphics.Text(Label, PosX + Settings.Text.X, PosY + Settings.Text.Y, Settings.Text.Font, Settings.Text.Scale, Settings.Text.Colors[Index][1], Settings.Text.Colors[Index][2], Settings.Text.Colors[Index][3], Settings.Text.Colors[Index][4], Settings.Text.Center, Settings.Text.Outline, Settings.Text.DropShadow) 107 | self.Options = self.Options + 1 108 | self.Offset = vector2(PosX, PosY) 109 | end 110 | 111 | function ContextUI:Visible() 112 | SetMouseCursorSprite(1) 113 | self.Menus[self.Entity.Type .. self.Category]() 114 | local X, Y = 1920, 1080 115 | local lastX, lastY = self.Offset.x, self.Offset.y 116 | if (lastY + (not self.Description and Settings.Button.Height or 0)) >= Y then 117 | self.Position = vector2(self.Position.x, self.Position.y - 10.0) 118 | end 119 | if (lastX + Settings.Button.Width) >= X then 120 | self.Position = vector2(self.Position.x - 10.0, self.Position.y) 121 | end 122 | self.Options = 0; 123 | self.Description = nil; 124 | end 125 | 126 | function ContextUI:CreateMenu(EntityType, Title) 127 | return { EntityType = EntityType, Category = "main", Parent = nil, Title = Title } 128 | end 129 | 130 | function ContextUI:CreateSubMenu(Parent, Title) 131 | local category = self.CategoryID + 1 132 | self.CategoryID = category; 133 | return { EntityType = Parent.EntityType, Category = category, Parent = Parent, Title = Title } 134 | end 135 | 136 | function ContextUI:IsVisible(Menu, Callback) 137 | self.Menus[Menu.EntityType .. Menu.Category] = function() 138 | if (Menu.Title) then 139 | ShowTitle(Menu.Title) 140 | end 141 | Callback(self.Entity) 142 | if Menu.Parent then 143 | self:Button("← Retour", nil, nil, Menu.Parent) 144 | end 145 | if (self.Description) then 146 | ShowDescription(self.Description) 147 | end 148 | end 149 | end 150 | 151 | Citizen.CreateThread(function() 152 | local controls_actions = { 239, 240, 24, 25 } 153 | while true do 154 | local Timer = 250; 155 | if (ContextUI.Focus) then 156 | DisableAllControlActions(2) 157 | SetMouseCursorActiveThisFrame() 158 | for _, control in ipairs(controls_actions) do 159 | EnableControlAction(0, control, true) 160 | end 161 | if (not ContextUI.Open) then 162 | local isFound, entityCoords, surfaceNormal, entityHit, entityType, cameraDirection, mouse = Graphics.ScreenToWorld(35.0, 31) 163 | if (entityType ~= 0) then 164 | SetMouseCursorSprite(5) 165 | if ContextUI.Entity.ID ~= entityHit then 166 | ResetEntityAlpha(ContextUI.Entity.ID) 167 | ContextUI.Entity.ID = entityHit 168 | SetEntityAlpha(ContextUI.Entity.ID, 200, false) 169 | end 170 | if IsControlJustPressed(0, 24) or IsDisabledControlPressed(0, 24) then 171 | if (ContextUI.Menus[entityType .. ContextUI.Category] ~= nil) then 172 | local posX, posY = Graphics.ConvertToPixel(mouse.x, mouse.y) 173 | ContextUI.Position = vector2(posX, posY) 174 | ContextUI.Entity = { 175 | ID = entityHit, 176 | Type = entityType, 177 | Model = GetEntityModel(entityHit) or 0, 178 | NetID = NetworkGetNetworkIdFromEntity(entityHit), 179 | ServerID = GetPlayerServerId(NetworkGetPlayerIndexFromPed(entityHit)) 180 | } 181 | ContextUI.Open = true 182 | Audio.PlaySound("HUD_FRONTEND_DEFAULT_SOUNDSET", "SELECT", false) 183 | end 184 | end 185 | else 186 | if (ContextUI.Entity.ID ~= nil) then 187 | ResetEntityAlpha(ContextUI.Entity.ID) 188 | ContextUI.Entity.ID = nil 189 | end 190 | SetMouseCursorSprite(1) 191 | end 192 | else 193 | ContextUI:Visible() 194 | end 195 | DisablePlayerFiring(PlayerPedId(), true) 196 | Timer = 1; 197 | elseif (ContextUI.Entity.ID ~= nil) then 198 | ContextUI:OnClosed() 199 | end 200 | Citizen.Wait(Timer) 201 | end 202 | end) 203 | --------------------------------------------------------------------------------