├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── conf.lua ├── love_demo.lua ├── love_minimal.lua ├── lovelogo.png ├── lovr_demo.lua ├── lovr_minimal.lua ├── lovrlogo.png ├── main.lua └── ui2d ├── DejaVuSansMono.ttf └── ui2d.lua /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | conf.lua -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 John Dodis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lovr-ui2d 2 | 3 | ### An immediate mode GUI library for the [LÖVR](https://lovr.org/) and [LÖVE](https://love2d.org/) frameworks. 4 | This is the sister project of [lovr-ui](https://github.com/immortalx74/lovr-ui) (a VR GUI library for lovr). 5 | Both projects borrow concepts from the outstanding [Dear ImGui](https://github.com/ocornut/imgui) library and are inspired by [microui](https://github.com/rxi/microui), trying to be simple and minimal. 6 | 7 | 8 | This was formerly 2 different branches, one for each framework. It's now a unified codebase since lovr and love have a very similar API. It has zero depedencies and it is pure Lua, meaning this is not bindings to a "foreign" library (which usually require a specific version of said library to work). 9 | 10 | https://github.com/immortalx74/lovr-ui2d/assets/29693328/3b1e15cc-948f-401f-a236-ee63c44e07ea 11 | 12 | **How to use:** 13 | 14 | See `main.lua` for minimal and demo implementations. Below is the complete API documentation but some things will make more sense by examining the examples. 15 | 16 | **Widgets:** 17 | 18 | - Button 19 | - ImageButton 20 | - TextBox 21 | - ListBox 22 | - SliderInt 23 | - SliderFloat 24 | - Label 25 | - CheckBox 26 | - ToggleButton 27 | - RadioButton 28 | - TabBar 29 | - Dummy 30 | - ProgressBar 31 | - CustomWidget 32 | - Modal window 33 | - Separator 34 | 35 | **API:** 36 | 37 | --- 38 | `UI2D.Button(name, width, height, tooltip)` 39 | |Argument|Type|Description 40 | |:---|:---|:---| 41 | |`name`|string|button's text 42 | |`width` _[opt]_|number|button width in pixels 43 | |`height` _[opt]_|number|button height in pixels 44 | |`tooltip` _[opt]_|string|tooltip text 45 | 46 | Returns: `boolean`, true when clicked. 47 | NOTE: if no `width` and/or `height` are provided, the button size will be auto-calculated based on text. Otherwise, it will be set to `width` X `height` (with the text centered) or ignored if that size doesn't fit the text. 48 | 49 | --- 50 | `UI2D.ImageButton(texture, width, height, text, tooltip)` 51 | |Argument|Type|Description 52 | |:---|:---|:---| 53 | |`texture`|texture/image|texture(lovr) or image(love) 54 | |`width`|number|image width in pixels 55 | |`height`|number|image height in pixels 56 | |`text` _[opt]_|string|optional text 57 | |`tooltip` _[opt]_|string|tooltip text 58 | 59 | Returns: `boolean` , true when clicked. 60 | 61 | --- 62 | `UI2D.CustomWidget(name, width, height, tooltip)` 63 | |Argument|Type|Description 64 | |:---|:---|:---| 65 | |`name`|string|custom widget name 66 | |`width`|number|width in pixels 67 | |`height`|number|height in pixels 68 | |`tooltip` _[opt]_|string|tooltip text 69 | 70 | Returns: `Pass(lovr) or Canvas(love)`, `boolean`, `boolean`, `boolean`, `boolean`, `number`, `number`, `number`, `number` [1] Pass object(lovr) or Canvas(love), [2] clicked, [3] down, [4] released, [5] hovered, [6] mouse X, [7] mouse Y, [8] wheel X, [9] wheel Y 71 | NOTE: General purpose widget for custom drawing/interaction. The returned Pass(lovr) or Canvas(love) can be used to do regular draw-commands. X and Y are the local 2D coordinates of the pointer (0,0 is top,left) 72 | 73 | --- 74 | `UI2D.TextBox(name, num_visible_chars, text, tooltip)` 75 | |Argument|Type|Description 76 | |:---|:---|:---| 77 | |`name`|string|textbox name 78 | |`num_visible_chars`|number|number of visible characters 79 | |`text`|string|text 80 | |`tooltip` _[opt]_|string|tooltip text 81 | 82 | Returns: `string`, `boolean` [1] text, [2] finished editing. 83 | NOTE: Always assign back to your string variable e.g. `mytext = UI2D.TextBox("My textbox, 10, mytext)`. To do validation on the edited text, check the finished editing return value. 84 | 85 | --- 86 | `UI2D.ListBox(name, num_visible_rows, num_visible_chars, collection, selected, multi_select, tooltip)` 87 | |Argument|Type|Description 88 | |:---|:---|:---| 89 | |`name`|string|listbox name 90 | |`num_visible_rows`|number|number of visible rows 91 | |`num_visible_chars`|number|number of visible characters on each row 92 | |`collection`|table|table of strings 93 | |`selected` _[opt]_|number or string|selected item index (in case it's a string, selects the 1st occurence of the item that matches the string) 94 | |`multi_select` _[opt]_|boolean|whether multi-select should be enabled 95 | |`tooltip` _[opt]_|string|tooltip text 96 | 97 | Returns: `boolean`, `number`, `table`, [1] true when clicked, [2] selected item index, [3] table of selected item indices (if multi_select is true) 98 | NOTE: The `UI2D.ListBoxSetSelected` helper can be used to select item(s) programmatically. 99 | 100 | --- 101 | `UI2D.SliderInt(name, v, v_min, v_max, width, tooltip)` 102 | |Argument|Type|Description 103 | |:---|:---|:---| 104 | |`name`|string|slider text 105 | |`v`|number|initial value 106 | |`v_min`|number|minimum value 107 | |`v_max`|number|maximum value 108 | |`width` _[opt]_|number|total width in pixels of the slider, including it's text 109 | |`tooltip` _[opt]_|string|tooltip text 110 | 111 | Returns: `number`, `boolean`, [1] current value, [2] true when released 112 | NOTE: Always assign back to your slider-value, e.g. `myval = UI2D.SliderInt("my slider", myval, 0, 100)` 113 | If width is provided, it will be taken into account only if it exceeds the width of text, otherwise it will be ignored. 114 | 115 | --- 116 | `UI2D.SliderFloat(name, v, v_min, v_max, width, num_decimals, tooltip)` 117 | |Argument|Type|Description 118 | |:---|:---|:---| 119 | |`name`|string|slider text 120 | |`v`|number|initial value 121 | |`v_min`|number|minimum value 122 | |`v_max`|number|maximum value 123 | |`width` _[opt]_|number|total width in pixels of the slider, including it's text 124 | |`num_decimals` _[opt]_|number|number of decimals to display 125 | |`tooltip` _[opt]_|string|tooltip text 126 | 127 | Returns: `number`, `boolean`, [1] current value, [2] true when released 128 | NOTE: Always assign back to your slider-value, e.g. `myval = UI2D.SliderFloat("my slider", myval, 0, 100)` 129 | If `width` is provided, it will be taken into account only if it exceeds the width of text, otherwise it will be ignored. If no `num_decimals` is provided, it defaults to 2. 130 | 131 | --- 132 | `UI2D.Label(text)` 133 | |Argument|Type|Description 134 | |:---|:---|:---| 135 | |`text`|string|label text 136 | |`compact` _[opt]_|boolean|ignore vertical margin 137 | 138 | Returns: `nothing` 139 | 140 | --- 141 | `UI2D.ProgressBar(progress, width, tooltip)` 142 | |Argument|Type|Description 143 | |:---|:---|:---| 144 | |`progress`|number|progress percentage 145 | |`width` _[opt]_|number|width in pixels 146 | |`tooltip` _[opt]_|string|tooltip text 147 | 148 | Returns: `nothing` 149 | NOTE: Default width is 300 pixels 150 | 151 | --- 152 | `UI2D.Separator()` 153 | |Argument|Type|Description 154 | |:---|:---|:---| 155 | |`none`|| 156 | 157 | Returns: `nothing` 158 | NOTE: Horizontal Separator 159 | 160 | --- 161 | `UI2D.CheckBox(text, checked, tooltip)` 162 | |Argument|Type|Description 163 | |:---|:---|:---| 164 | |`text`|string|checkbox text 165 | |`checked`|boolean|state 166 | |`tooltip` _[opt]_|string|tooltip text 167 | 168 | Returns: `boolean`, true when clicked 169 | NOTE: To set the state use this idiom: `if UI2D.CheckBox("My checkbox", my_state) then my_state = not my_state end` 170 | 171 | --- 172 | `UI2D.ToggleButton(text, checked, tooltip)` 173 | |Argument|Type|Description 174 | |:---|:---|:---| 175 | |`text`|string|toggle button text 176 | |`checked`|boolean|state 177 | |`tooltip` _[opt]_|string|tooltip text 178 | 179 | Returns: `boolean`, true when clicked 180 | NOTE: To set the state use this idiom: `if UI2D.ToggleButton("My toggle button", my_state) then my_state = not my_state end` 181 | 182 | --- 183 | `UI2D.RadioButton(text, checked, tooltip)` 184 | |Argument|Type|Description 185 | |:---|:---|:---| 186 | |`text`|string|radiobutton text 187 | |`checked`|boolean|state 188 | |`tooltip` _[opt]_|string|tooltip text 189 | 190 | Returns: `boolean`, true when clicked 191 | NOTE: To set the state on a group of RadioButtons use this idiom: 192 | `if UI2D.RadioButton("Radio1", rb_group_idx == 1) then rb_group_idx = 1 end` 193 | `if UI2D.RadioButton("Radio2", rb_group_idx == 2) then rb_group_idx = 2 end` 194 | `-- etc...` 195 | 196 | --- 197 | `UI2D.TabBar(name, tabs, idx, tooltip)` 198 | |Argument|Type|Description 199 | |:---|:---|:---| 200 | |`name`|string|TabBar name 201 | |`tabs`|table|a table of strings 202 | |`idx`|number|initial active tab index 203 | |`tooltip` _[opt]_|string|tooltip text 204 | 205 | Returns: `boolean`, `number`, [1] true when clicked, [2] the selected tab index 206 | 207 | --- 208 | `UI2D.Dummy(width, height)` 209 | |Argument|Type|Description 210 | |:---|:---|:---| 211 | |`width`|number|width 212 | |`height`|number|height 213 | 214 | Returns: `nothing` 215 | NOTE: This is an invisible widget useful only to "push" other widgets' positions or to leave a desired gap. 216 | 217 | --- 218 | `UI2D.Begin(name, x, y, is_modal)` 219 | |Argument|Type|Description 220 | |:---|:---|:---| 221 | |`name`|string|window title 222 | |`x`|number|window X position 223 | |`y`|number|window Y position 224 | |`is_modal` _[opt]_|boolean|is this a modal window 225 | 226 | Returns: `nothing` 227 | NOTE: Starts a new window. Every widget call after this function will belong to this window, until `UI2D.End()` is called. If this is set as a modal window (by passing true to the last argument) you should always call `UI2D.EndModalWindow` before closing it physically. 228 | 229 | --- 230 | `UI2D.End(main_pass(lovr) or nothing(love))` 231 | |Argument|Type|Description 232 | |:---|:---|:---| 233 | |`main_pass`|Pass|the main Pass object(only for lovr) 234 | 235 | Returns: `nothing` 236 | NOTE: Ends the current window. 237 | 238 | --- 239 | `UI2D.SameLine()` 240 | |Argument|Type|Description 241 | |:---|:---|:---| 242 | |`none`|| 243 | 244 | Returns: `nothing` 245 | NOTE: Places the next widget side-to-side with the last one, instead of bellow 246 | 247 | --- 248 | `UI2D.GetWindowSize(name)` 249 | |Argument|Type|Description 250 | |:---|:---|:---| 251 | |`name`|number|window name 252 | 253 | Returns: `number`, `number`, [1] window width, [2] window height 254 | NOTE: If no window with this name was found, return type is `nil` 255 | 256 | --- 257 | `UI2D.Init(type, size)` 258 | |Argument|Type|Description 259 | |:---|:---|:---| 260 | |`type`|string|which framework to use (valid values: "lovr", "love") 261 | |`size` _[opt]_|number|font size 262 | 263 | Returns: `nothing` 264 | NOTE: Initializes the library and should be called on `lovr/love.load()`. Font size dictates the general size of the UI. Default is 14 265 | 266 | --- 267 | `UI2D.InputInfo()` 268 | |Argument|Type|Description 269 | |:---|:---|:---| 270 | |`none`|| 271 | 272 | Returns: `nothing` 273 | NOTE: Should be called on `lovr/love.update()` 274 | 275 | --- 276 | `UI2D.RenderFrame(main_pass(only for lovr))` 277 | |Argument|Type|Description 278 | |:---|:---|:---| 279 | |`main_pass`|Pass|the main Pass object(lovr). 280 | 281 | Returns: `table` of ui passes(lovr) or nothing(love) 282 | NOTE: Renders the UI. Should be called in `lovr/love.draw()`. (If you're using lovr see the examples on how to handle the passes returned from this call.) 283 | 284 | --- 285 | `UI2D.OverrideColor(col_name, color)` 286 | |Argument|Type|Description 287 | |:---|:---|:---| 288 | |`col_name`|string|color name 289 | |`color`|table|color value in table form (r, g, b, a) 290 | 291 | Returns: `nothing` 292 | NOTE: Helper to override a color value. 293 | 294 | --- 295 | `UI2D.SetColorTheme(theme, copy_from)` 296 | |Argument|Type|Description 297 | |:---|:---|:---| 298 | |`theme`|string or table|color name or table with names of colors 299 | |`copy_from` _[opt]_|string|color-theme to copy values from 300 | 301 | Returns: `nothing` 302 | NOTE: Sets the color-theme to one of the built-in ones ("dark", "light") if the passed argument is a string. Also accepts a table of colors. If the passed table doesn't contain all of the keys, the rest of them will be copied from the built-in theme of the `copy_from` argument. 303 | 304 | --- 305 | `UI2D.CloseModalWindow()` 306 | |Argument|Type|Description 307 | |:---|:---|:---| 308 | |`none`|| 309 | 310 | Returns: `nothing` 311 | NOTE: Closes a modal window 312 | 313 | --- 314 | `UI2D.KeyPressed(key, repeating)` 315 | |Argument|Type|Description 316 | |:---|:---|:---| 317 | |`key`|string|key name 318 | |`repeating`|boolean|if the key is repeating instead of an instant press. 319 | 320 | Returns: `nothing` 321 | NOTE: Should be called on `lovr/love.keypressed()` callback. 322 | 323 | --- 324 | `UI2D.TextInput(text)` 325 | |Argument|Type|Description 326 | |:---|:---|:---| 327 | |`text`|string|character from a textinput event. 328 | 329 | Returns: `nothing` 330 | NOTE: Should be called on `lovr/love.textinput()` callback. 331 | 332 | --- 333 | `UI2D.KeyReleased()` 334 | |Argument|Type|Description 335 | |:---|:---|:---| 336 | |`none`|| 337 | 338 | Returns: `nothing` 339 | NOTE: Should be called on `lovr/love.keyreleased()` callback. 340 | 341 | --- 342 | `UI2D.WheelMoved(x, y)` 343 | |Argument|Type|Description 344 | |:---|:---|:---| 345 | |`x`|number|wheel X. 346 | |`y`|number|wheel Y. 347 | 348 | Returns: `nothing` 349 | NOTE: Should be called on `lovr/love.wheelmoved()` callback. 350 | 351 | --- 352 | `UI2D.HasMouse()` 353 | |Argument|Type|Description 354 | |:---|:---|:---| 355 | |`none`|| 356 | 357 | Returns: `nothing` 358 | NOTE: Whether the mouse-pointer hovers a UI2D window. 359 | 360 | --- 361 | `UI2D.SetWindowPosition(name, x, y)` 362 | |Argument|Type|Description 363 | |:---|:---|:---| 364 | |`name`|string|name of the window 365 | |`x`|number|X position 366 | |`y`|number|Y position 367 | 368 | Returns: `boolean`, true if the window was found 369 | NOTE: Sets a window's position programmatically. 370 | 371 | --- 372 | `UI2D.GetColorTheme()` 373 | |Argument|Type|Description 374 | |:---|:---|:---| 375 | |`none`|| 376 | 377 | Returns: `string`, theme name 378 | NOTE: Gets the current color-theme 379 | 380 | --- 381 | `UI2D.ResetColor(col_name)` 382 | |Argument|Type|Description 383 | |:---|:---|:---| 384 | |`col_name`|string|color name 385 | 386 | Returns: `nothing` 387 | NOTE: Resets a color to its default value 388 | 389 | --- 390 | `UI2D.SetFontSize(size)` 391 | |Argument|Type|Description 392 | |:---|:---|:---| 393 | |`size`|number|font size 394 | 395 | Returns: `nothing` 396 | NOTE: Sets the font size 397 | 398 | --- 399 | `UI2D.GetFontSize()` 400 | |Argument|Type|Description 401 | |:---|:---|:---| 402 | |`none`|| 403 | 404 | Returns: `number`, font size 405 | NOTE: Gets the current font size 406 | 407 | --- 408 | `UI2D.HasTextInput()` 409 | |Argument|Type|Description 410 | |:---|:---|:---| 411 | |`none`|| 412 | 413 | Returns: `boolean`, true if a textbox has focus 414 | NOTE: Gets whether the text of a textbox is currently being edited 415 | 416 | --- 417 | `UI2D.IsModalOpen()` 418 | |Argument|Type|Description 419 | |:---|:---|:---| 420 | |`none`|| 421 | 422 | Returns: `boolean`, true if a modal window is currently open 423 | NOTE: Gets whether a modal window is currently open 424 | 425 | --- 426 | `UI2D.EndModalWindow()` 427 | |Argument|Type|Description 428 | |:---|:---|:---| 429 | |`none`|| 430 | 431 | Returns: `nothing` 432 | NOTE: Informs UI2D that a previously open modal-window was closed. You should always call this when closing a modal-window (usually performed from a button inside that window) so that UI2D can restore interaction with the other windows. 433 | 434 | --- 435 | `UI2D.SameColumn()` 436 | |Argument|Type|Description 437 | |:---|:---|:---| 438 | |`none`|| 439 | 440 | Returns: `nothing` 441 | NOTE: If the last widget used the `UI2D.SameLine()` call, it effectively started a new "column". This function can be called such as the next widget will be placed on that column, under the last widget. 442 | 443 | --- 444 | `UI2D.ListBoxSetSelected(name, idx)` 445 | |Argument|Type|Description 446 | |:---|:---|:---| 447 | |`name`|string|listbox name 448 | |`idx`|number or table|Index of item to be selected, or table of indices (in case this listbox' multi_select property is set to true) 449 | 450 | Returns: `nothing` 451 | NOTE: Sets the selected item(s) of a ListBox programmatically 452 | -------------------------------------------------------------------------------- /conf.lua: -------------------------------------------------------------------------------- 1 | if love then 2 | function love.conf( t ) 3 | t.window.width = 1300 4 | t.window.height = 600 5 | t.window.resizable = true 6 | t.console = true 7 | t.modules.joystick = false 8 | end 9 | else 10 | function lovr.conf( t ) 11 | t.modules.headset = false 12 | t.window.resizable = true 13 | t.window.width = 1300 14 | t.window.height = 600 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /love_demo.lua: -------------------------------------------------------------------------------- 1 | UI2D = require "ui2d..ui2d" 2 | 3 | local sl1 = 20 4 | local sl2 = 10 5 | local sl3 = 10.3 6 | local icon = love.graphics.newImage( "lovelogo.png" ) 7 | local tab_bar_idx = 1 8 | local check1 = true 9 | local check2 = false 10 | local toggle1 = false 11 | local toggle2 = true 12 | local rb_idx = 1 13 | local progress = { value = 0, adder = 0 } 14 | local txt1 = "482.32" 15 | local txt2 = "a bigger textbox" 16 | local amplitude = 50 17 | local frequency = 0.1 18 | local modal_window_open = false 19 | local some_list = { "fade", "wrong", "milky", "zinc", "doubt", "proud", "well-to-do", 20 | "carry", "knife", "ordinary", "yielding", "yawn", "salt", "examine", "historical", 21 | "group", "certain", "disgusting", "hum", "left", "camera", "grey", "memorize", 22 | "squalid", "second-hand", "domineering", "puzzled", "cloudy", "arrogant", "flat", 23 | "activity", "obedient", "poke", "power", "brave", "ruthless", "knowing", "shut", 24 | "crook", "base", "pleasure", "cycle", "kettle", "regular", "substantial", "flowery", 25 | "industrious", "credit", "rice", "harm", "nifty", "boiling", "get", "volleyball", 26 | "jobless", "honey", "piquant", "desire", "glossy", "spark", "hulking", "leg", "hurry" } 27 | 28 | -- Helper function to draw a CustomWidget 29 | local function DrawMyCustomWidget( tex, held, hovered, mx, my ) 30 | if held then 31 | amplitude = (75 * my) / 150 32 | frequency = (0.2 * mx) / 250 33 | end 34 | 35 | local col = { 0, 0, 0 } 36 | if hovered then 37 | col = { 0.1, 0, 0.2 } 38 | end 39 | 40 | -- Prepare this custom-widget's canvas 41 | love.graphics.setCanvas( tex ) 42 | love.graphics.clear( col ) 43 | love.graphics.setColor( 1, 1, 1 ) 44 | 45 | local xx = 0 46 | local yy = 0 47 | local y = 75 48 | 49 | for i = 1, 250 do 50 | yy = y + (amplitude * math.sin( frequency * xx )) 51 | love.graphics.points( xx, yy ) 52 | xx = xx + 1 53 | end 54 | end 55 | 56 | function love.load() 57 | -- Initialize the library. You can optionally pass a font size. Default is 14. 58 | UI2D.Init( "love" ) 59 | end 60 | 61 | function love.keypressed( key, scancode, isrepeat ) 62 | UI2D.KeyPressed( key, isrepeat ) 63 | end 64 | 65 | function love.textinput( text ) 66 | UI2D.TextInput( text ) 67 | end 68 | 69 | function love.keyreleased( key, scancode ) 70 | UI2D.KeyReleased() 71 | end 72 | 73 | function love.wheelmoved( x, y ) 74 | UI2D.WheelMoved( x, y ) 75 | end 76 | 77 | function love.update( dt ) 78 | -- This gets input information for the library. 79 | UI2D.InputInfo() 80 | end 81 | 82 | function love.draw() 83 | love.graphics.clear( 0.2, 0.2, 0.7 ) 84 | 85 | -- Every window should be contained in a Begin/End block. This is the start of the first window. 86 | UI2D.Begin( "First Window", 50, 200 ) 87 | if UI2D.Button( "first button" ) then 88 | print( "from 1st button" ) 89 | end 90 | if UI2D.ImageButton( icon, 32, 32, "img button" ) then 91 | print( "img" ) 92 | end 93 | if UI2D.RadioButton( "Radio1", rb_idx == 1 ) then 94 | rb_idx = 1 95 | end 96 | if UI2D.RadioButton( "Radio2", rb_idx == 2 ) then 97 | rb_idx = 2 98 | end 99 | if UI2D.RadioButton( "Radio3", rb_idx == 3 ) then 100 | rb_idx = 3 101 | end 102 | if UI2D.Button( "Change theme" ) then 103 | if UI2D.GetColorTheme() == "light" then 104 | UI2D.SetColorTheme( "dark" ) 105 | else 106 | UI2D.SetColorTheme( "light" ) 107 | end 108 | end 109 | UI2D.End() -- And this is the end of the first window. 110 | 111 | -- More windows... 112 | UI2D.Begin( "Second Window", 250, 50 ) 113 | UI2D.Label( "We're doing progress...", true ) 114 | progress.adder = progress.adder + (10 * love.timer.getDelta()) 115 | if progress.adder > 100 then progress.adder = 0 end 116 | progress.value = math.floor( progress.adder ) 117 | UI2D.ProgressBar( progress.value ) 118 | UI2D.Separator() 119 | if UI2D.Button( "Font size +" ) then 120 | UI2D.SetFontSize( UI2D.GetFontSize() + 1 ) 121 | end 122 | UI2D.SameLine() 123 | if UI2D.Button( "Font size -" ) then 124 | UI2D.SetFontSize( UI2D.GetFontSize() - 1 ) 125 | end 126 | sl1, released = UI2D.SliderInt( "another slider", sl1, 0, 100, 296 ) 127 | if released then 128 | print( released, sl1 ) 129 | end 130 | if UI2D.ToggleButton( "Toggle1", toggle1 ) then 131 | toggle1 = not toggle1 132 | end 133 | if UI2D.ToggleButton( "Toggle2", toggle2 ) then 134 | toggle2 = not toggle2 135 | end 136 | UI2D.Label( "Widgets on same line", true ) 137 | UI2D.Button( "Hello", 80, nil, "This is a Tooltip" ) 138 | UI2D.SameLine() 139 | UI2D.Button( "World!", 80, nil, "And this is\na multi-line\nTooltip" ) 140 | UI2D.End() 141 | 142 | UI2D.Begin( "utf8 text support: ΞΔΠΘ", 950, 50 ) 143 | if UI2D.Button( "Open modal window" ) then 144 | modal_window_open = true 145 | end 146 | UI2D.OverrideColor( "button_bg", { 0.8, 0, 0.8 } ) 147 | UI2D.Button( "colored button" ) 148 | 149 | local clicked, idx = UI2D.ListBox( "list1", 15, 28, some_list ) 150 | if clicked then 151 | print( "selected item: " .. idx .. " - " .. some_list[ idx ] ) 152 | end 153 | UI2D.ResetColor( "button_bg" ) 154 | UI2D.Button( "Click me" ) 155 | UI2D.SameLine() 156 | sl2 = UI2D.SliderInt( "int slider", sl2, 0, 100 ) 157 | UI2D.End() 158 | 159 | UI2D.OverrideColor( "window_bg", { 0.1, 0.2, 0.6 } ) 160 | UI2D.Begin( "Colored window", 600, 300 ) 161 | UI2D.Button( "sample text" ) 162 | UI2D.SameLine() 163 | txt1, finished_editing = UI2D.TextBox( "textbox1", 11, txt1 ) 164 | if finished_editing then 165 | if type( tonumber( txt1 ) ) ~= "number" then 166 | txt1 = "0" 167 | end 168 | end 169 | txt2 = UI2D.TextBox( "textbox2", 25, txt2 ) 170 | if UI2D.CheckBox( "Really?", check1 ) then 171 | check1 = not check1 172 | end 173 | if UI2D.CheckBox( "Check me too", check2 ) then 174 | check2 = not check2 175 | end 176 | 177 | sl3 = UI2D.SliderFloat( "float slider", sl3, 0, 100, 300 ) 178 | UI2D.End() 179 | UI2D.ResetColor( "window_bg" ) 180 | 181 | UI2D.Begin( "TabBar window", 300, 390 ) 182 | local was_clicked, idx = UI2D.TabBar( "my tab bar", { "first", "second", "third" }, tab_bar_idx ) 183 | if was_clicked then 184 | tab_bar_idx = idx 185 | end 186 | if tab_bar_idx == 1 then 187 | UI2D.Button( "Button on 1st tab" ) 188 | UI2D.Label( "Label on 1st tab" ) 189 | UI2D.Label( "LÖVE..." ) 190 | elseif tab_bar_idx == 2 then 191 | UI2D.Button( "Button on 2nd tab" ) 192 | UI2D.Label( "Label on 2nd tab" ) 193 | UI2D.Label( "is..." ) 194 | elseif tab_bar_idx == 3 then 195 | UI2D.Button( "Button on 3rd tab" ) 196 | UI2D.Label( "Label on 3rd tab" ) 197 | UI2D.Label( "awesome!" ) 198 | end 199 | UI2D.End() 200 | 201 | UI2D.Begin( "Another window", 600, 50 ) 202 | UI2D.Label( "This is a custom widget" ) 203 | local tex, clicked, held, released, hovered, mx, my, wheelx, wheely = UI2D.CustomWidget( "widget1", 250, 150 ) 204 | DrawMyCustomWidget( tex, held, hovered, mx, my ) 205 | UI2D.End() 206 | 207 | -- A modal window is like all other windows, except there can only be one open at a time. 208 | -- This is set by passing 'true' as the last parameter of Begin(). 209 | -- When it's time to close a modal window ALWAYS call EndModalWindow() 210 | if modal_window_open then 211 | UI2D.Begin( "Modal window", 400, 200, true ) 212 | UI2D.Label( "Close this window\nto interact with other windows" ) 213 | if UI2D.Button( "Close" ) then 214 | modal_window_open = false 215 | UI2D.EndModalWindow() 216 | end 217 | UI2D.End() 218 | end 219 | 220 | -- This marks the end of the GUI. 221 | UI2D.RenderFrame() 222 | end 223 | -------------------------------------------------------------------------------- /love_minimal.lua: -------------------------------------------------------------------------------- 1 | UI2D = require "ui2d..ui2d" 2 | 3 | function love.load() 4 | -- Initialize the library. You can optionally pass a font size. Default is 14. 5 | UI2D.Init( "love" ) 6 | end 7 | 8 | function love.keypressed( key, scancode, isrepeat ) 9 | UI2D.KeyPressed( key, isrepeat ) 10 | end 11 | 12 | function love.textinput( text ) 13 | UI2D.TextInput( text ) 14 | end 15 | 16 | function love.keyreleased( key, scancode ) 17 | UI2D.KeyReleased() 18 | end 19 | 20 | function love.wheelmoved( x, y ) 21 | UI2D.WheelMoved( x, y ) 22 | end 23 | 24 | function love.update( dt ) 25 | -- This gets input information for the library. 26 | UI2D.InputInfo() 27 | end 28 | 29 | function love.draw( ) 30 | love.graphics.clear( 0.2, 0.2, 0.7 ) 31 | 32 | -- Every window should be contained in a Begin/End block. 33 | UI2D.Begin( "My Window", 200, 200 ) 34 | UI2D.Button( "My First Button" ) 35 | UI2D.End( pass ) 36 | 37 | -- This marks the end of the GUI. 38 | UI2D.RenderFrame() 39 | end 40 | -------------------------------------------------------------------------------- /lovelogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immortalx74/lovr-ui2d/513a090abfc3d44769662ed58e27bad3d321609e/lovelogo.png -------------------------------------------------------------------------------- /lovr_demo.lua: -------------------------------------------------------------------------------- 1 | UI2D = require "ui2d..ui2d" 2 | 3 | lovr.graphics.setBackgroundColor( 0.2, 0.2, 0.7 ) 4 | local sl1 = 20 5 | local sl2 = 10 6 | local sl3 = 10.3 7 | local icon = lovr.graphics.newTexture( "lovrlogo.png" ) 8 | local tab_bar_idx = 1 9 | local check1 = true 10 | local check2 = false 11 | local toggle1 = false 12 | local toggle2 = true 13 | local rb_idx = 1 14 | local progress = { value = 0, adder = 0 } 15 | local txt1 = "482.32" 16 | local txt2 = "a bigger textbox" 17 | local amplitude = 50 18 | local frequency = 0.1 19 | local modal_window_open = false 20 | local some_list = { "fade", "wrong", "milky", "zinc", "doubt", "proud", "well-to-do", 21 | "carry", "knife", "ordinary", "yielding", "yawn", "salt", "examine", "historical", 22 | "group", "certain", "disgusting", "hum", "left", "camera", "grey", "memorize", 23 | "squalid", "second-hand", "domineering", "puzzled", "cloudy", "arrogant", "flat", 24 | "activity", "obedient", "poke", "power", "brave", "ruthless", "knowing", "shut", 25 | "crook", "base", "pleasure", "cycle", "kettle", "regular", "substantial", "flowery", 26 | "industrious", "credit", "rice", "harm", "nifty", "boiling", "get", "volleyball", 27 | "jobless", "honey", "piquant", "desire", "glossy", "spark", "hulking", "leg", "hurry" } 28 | 29 | -- Helper function to draw a CustomWidget 30 | local function DrawMyCustomWidget( ps, held, hovered, mx, my ) 31 | if held then 32 | amplitude = (75 * my) / 150 33 | frequency = (0.2 * mx) / 250 34 | end 35 | 36 | local col = { 0, 0, 0 } 37 | if hovered then 38 | col = { 0.1, 0, 0.2 } 39 | end 40 | 41 | ps:setClear( col ) 42 | ps:setColor( 1, 1, 1 ) 43 | 44 | local xx = 0 45 | local yy = 0 46 | local y = 75 47 | 48 | for i = 1, 250 do 49 | yy = y + (amplitude * math.sin( frequency * xx )) 50 | ps:points( xx, yy, 0 ) 51 | xx = xx + 1 52 | end 53 | end 54 | 55 | function lovr.load() 56 | -- Initialize the library. You can optionally pass a font size. Default is 14. 57 | UI2D.Init( "lovr" ) 58 | end 59 | 60 | function lovr.keypressed( key, scancode, repeating ) 61 | UI2D.KeyPressed( key, repeating ) 62 | end 63 | 64 | function lovr.textinput( text, code ) 65 | UI2D.TextInput( text ) 66 | end 67 | 68 | function lovr.keyreleased( key, scancode ) 69 | UI2D.KeyReleased() 70 | end 71 | 72 | function lovr.wheelmoved( deltaX, deltaY ) 73 | UI2D.WheelMoved( deltaX, deltaY ) 74 | end 75 | 76 | function lovr.update( dt ) 77 | -- This gets input information for the library. 78 | UI2D.InputInfo() 79 | end 80 | 81 | function lovr.update( dt ) 82 | -- This gets input information for the library. 83 | UI2D.InputInfo() 84 | end 85 | 86 | function lovr.draw( pass ) 87 | pass:setProjection( 1, mat4():orthographic( pass:getDimensions() ) ) 88 | 89 | -- Every window should be contained in a Begin/End block. This is the start of the first window. 90 | UI2D.Begin( "First Window", 50, 200 ) 91 | if UI2D.Button( "first button" ) then 92 | print( "from 1st button" ) 93 | end 94 | if UI2D.ImageButton( icon, 32, 32, "img button" ) then 95 | print( "img" ) 96 | end 97 | if UI2D.RadioButton( "Radio1", rb_idx == 1 ) then 98 | rb_idx = 1 99 | end 100 | if UI2D.RadioButton( "Radio2", rb_idx == 2 ) then 101 | rb_idx = 2 102 | end 103 | if UI2D.RadioButton( "Radio3", rb_idx == 3 ) then 104 | rb_idx = 3 105 | end 106 | if UI2D.Button( "Change theme" ) then 107 | if UI2D.GetColorTheme() == "light" then 108 | UI2D.SetColorTheme( "dark" ) 109 | else 110 | UI2D.SetColorTheme( "light" ) 111 | end 112 | end 113 | UI2D.End( pass ) -- And this is the end of the first window. 114 | 115 | -- More windows... 116 | UI2D.Begin( "Second Window", 250, 50 ) 117 | UI2D.Label( "We're doing progress...", true ) 118 | progress.adder = progress.adder + (10 * lovr.timer.getDelta()) 119 | if progress.adder > 100 then progress.adder = 0 end 120 | progress.value = math.floor( progress.adder ) 121 | UI2D.ProgressBar( progress.value ) 122 | UI2D.Separator() 123 | if UI2D.Button( "Font size +" ) then 124 | UI2D.SetFontSize( UI2D.GetFontSize() + 1 ) 125 | end 126 | UI2D.SameLine() 127 | if UI2D.Button( "Font size -" ) then 128 | UI2D.SetFontSize( UI2D.GetFontSize() - 1 ) 129 | end 130 | sl1, released = UI2D.SliderInt( "another slider", sl1, 0, 100, 296 ) 131 | if released then 132 | print( released, sl1 ) 133 | end 134 | if UI2D.ToggleButton( "Toggle1", toggle1 ) then 135 | toggle1 = not toggle1 136 | end 137 | if UI2D.ToggleButton( "Toggle2", toggle2 ) then 138 | toggle2 = not toggle2 139 | end 140 | UI2D.Label( "Widgets on same line", true ) 141 | UI2D.Button( "Hello", 80, nil, "This is a Tooltip" ) 142 | UI2D.SameLine() 143 | UI2D.Button( "World!", 80, nil, "And this is\na multi-line\nTooltip" ) 144 | UI2D.End( pass ) 145 | 146 | UI2D.Begin( "utf8 text support: ΞΔΠΘ", 950, 50 ) 147 | if UI2D.Button( "Open modal window" ) then 148 | modal_window_open = true 149 | end 150 | UI2D.OverrideColor( "button_bg", { 0.8, 0, 0.8 } ) 151 | UI2D.Button( "colored button" ) 152 | 153 | local clicked, idx = UI2D.ListBox( "list1", 15, 28, some_list ) 154 | if clicked then 155 | print( "selected item: " .. idx .. " - " .. some_list[ idx ] ) 156 | end 157 | UI2D.ResetColor( "button_bg" ) 158 | UI2D.Button( "Click me" ) 159 | UI2D.SameLine() 160 | sl2 = UI2D.SliderInt( "int slider", sl2, 0, 100 ) 161 | UI2D.End( pass ) 162 | 163 | UI2D.OverrideColor( "window_bg", { 0.1, 0.2, 0.6 } ) 164 | UI2D.Begin( "Colored window", 600, 300 ) 165 | UI2D.Button( "sample text" ) 166 | UI2D.SameLine() 167 | txt1, finished_editing = UI2D.TextBox( "textbox1", 11, txt1 ) 168 | if finished_editing then 169 | if type( tonumber( txt1 ) ) ~= "number" then 170 | txt1 = "0" 171 | end 172 | end 173 | txt2 = UI2D.TextBox( "textbox2", 25, txt2 ) 174 | if UI2D.CheckBox( "Really?", check1 ) then 175 | check1 = not check1 176 | end 177 | if UI2D.CheckBox( "Check me too", check2 ) then 178 | check2 = not check2 179 | end 180 | 181 | sl3 = UI2D.SliderFloat( "float slider", sl3, 0, 100, 300 ) 182 | UI2D.End( pass ) 183 | UI2D.ResetColor( "window_bg" ) 184 | 185 | UI2D.Begin( "TabBar window", 300, 390 ) 186 | local was_clicked, idx = UI2D.TabBar( "my tab bar", { "first", "second", "third" }, tab_bar_idx ) 187 | if was_clicked then 188 | tab_bar_idx = idx 189 | end 190 | if tab_bar_idx == 1 then 191 | UI2D.Button( "Button on 1st tab" ) 192 | UI2D.Label( "Label on 1st tab" ) 193 | UI2D.Label( "LÖVR..." ) 194 | elseif tab_bar_idx == 2 then 195 | UI2D.Button( "Button on 2nd tab" ) 196 | UI2D.Label( "Label on 2nd tab" ) 197 | UI2D.Label( "is..." ) 198 | elseif tab_bar_idx == 3 then 199 | UI2D.Button( "Button on 3rd tab" ) 200 | UI2D.Label( "Label on 3rd tab" ) 201 | UI2D.Label( "awesome!" ) 202 | end 203 | UI2D.End( pass ) 204 | 205 | UI2D.Begin( "Another window", 600, 50 ) 206 | UI2D.Label( "This is a custom widget" ) 207 | local ps, clicked, held, released, hovered, mx, my, wheelx, wheely = UI2D.CustomWidget( "widget1", 250, 150 ) 208 | DrawMyCustomWidget( ps, held, hovered, mx, my ) 209 | UI2D.End( pass ) 210 | 211 | -- A modal window is like all other windows, except there can only be one open at a time. 212 | -- This is set by passing 'true' as the last parameter of Begin(). 213 | -- When it's time to close a modal window ALWAYS call EndModalWindow() 214 | if modal_window_open then 215 | UI2D.Begin( "Modal window", 400, 200, true ) 216 | UI2D.Label( "Close this window\nto interact with other windows" ) 217 | if UI2D.Button( "Close" ) then 218 | modal_window_open = false 219 | UI2D.EndModalWindow() 220 | end 221 | UI2D.End( pass ) 222 | end 223 | 224 | -- This marks the end of the GUI. 225 | -- RenderFrame returns a table of passes generated by UI2D. 226 | -- Insert the main pass into that table and call lovr.graphics.submit. 227 | local ui_passes = UI2D.RenderFrame( pass ) 228 | table.insert( ui_passes, pass ) 229 | return lovr.graphics.submit( ui_passes ) 230 | end 231 | -------------------------------------------------------------------------------- /lovr_minimal.lua: -------------------------------------------------------------------------------- 1 | UI2D = require "ui2d..ui2d" 2 | lovr.graphics.setBackgroundColor( 0.2, 0.2, 0.7 ) 3 | 4 | function lovr.load() 5 | -- Initialize the library. You can optionally pass a font size. Default is 14. 6 | UI2D.Init( "lovr" ) 7 | end 8 | 9 | function lovr.keypressed( key, scancode, repeating ) 10 | UI2D.KeyPressed( key, repeating ) 11 | end 12 | 13 | function lovr.textinput( text, code ) 14 | UI2D.TextInput( text ) 15 | end 16 | 17 | function lovr.keyreleased( key, scancode ) 18 | UI2D.KeyReleased() 19 | end 20 | 21 | function lovr.wheelmoved( deltaX, deltaY ) 22 | UI2D.WheelMoved( deltaX, deltaY ) 23 | end 24 | 25 | function lovr.update( dt ) 26 | -- This gets input information for the library. 27 | UI2D.InputInfo() 28 | end 29 | 30 | function lovr.draw( pass ) 31 | pass:setProjection( 1, mat4():orthographic( pass:getDimensions() ) ) 32 | 33 | -- Every window should be contained in a Begin/End block. 34 | UI2D.Begin( "My Window", 200, 200 ) 35 | UI2D.Button( "My First Button" ) 36 | UI2D.End( pass ) 37 | 38 | -- This marks the end of the GUI. 39 | -- RenderFrame returns a table of passes generated by UI2D. 40 | -- Insert the main pass into that table and call lovr.graphics.submit. 41 | local ui_passes = UI2D.RenderFrame( pass ) 42 | table.insert( ui_passes, pass ) 43 | return lovr.graphics.submit( ui_passes ) 44 | end 45 | -------------------------------------------------------------------------------- /lovrlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immortalx74/lovr-ui2d/513a090abfc3d44769662ed58e27bad3d321609e/lovrlogo.png -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | -- Bellow are 2 examples per framework (love/lovr) 2 | -- "minimal" is the minimal ui2d implementation and "demo" is a demonstration of all widgets in the library 3 | 4 | if lovr then 5 | require "lovr_demo" 6 | -- require "lovr_minimal" 7 | else 8 | require "love_demo" 9 | -- require "love_minimal" 10 | end 11 | -------------------------------------------------------------------------------- /ui2d/DejaVuSansMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immortalx74/lovr-ui2d/513a090abfc3d44769662ed58e27bad3d321609e/ui2d/DejaVuSansMono.ttf -------------------------------------------------------------------------------- /ui2d/ui2d.lua: -------------------------------------------------------------------------------- 1 | local utf8 = require "utf8" 2 | 3 | local UI2D = {} 4 | local framework = {} 5 | 6 | local has_text_input = false 7 | local has_mouse = false 8 | local e_mouse_state = { clicked = 1, held = 2, released = 3, idle = 4 } 9 | local e_slider_type = { int = 1, float = 2 } 10 | local modal_window = nil 11 | local active_window = nil 12 | local active_widget = nil 13 | local active_textbox = nil 14 | local dragged_window = nil 15 | local repeating_key = nil 16 | local text_input_character = nil 17 | local begin_idx = nil 18 | local margin = 8 19 | local next_z = 0 20 | local separator_thickness = 2 21 | local begin_end_pairs = { b = 0, e = 0 } 22 | local windows = {} 23 | local color_themes = {} 24 | local overriden_colors = {} 25 | local listbox_state = {} 26 | local caret_blink = { prev = 0, on = false } 27 | local font = { handle = nil, w = nil, h = nil } 28 | local dragged_window_offset = { x = 0, y = 0 } 29 | local mouse = { x = 0, y = 0, state = e_mouse_state.idle, prev_frame = 0, this_frame = 0, wheel_x = 0, wheel_y = 0 } 30 | local layout = { x = 0, y = 0, w = 0, h = 0, row_h = 0, total_w = 0, total_h = 0, same_line = false, same_column = false } 31 | local texture_flags = { mipmaps = true, usage = { 'sample', 'render', 'transfer' } } 32 | local clamp_sampler 33 | local active_tooltip = { text = "", x = 0, y = 0 } 34 | 35 | local keys = { 36 | [ "right" ] = { 0, 0, 0 }, 37 | [ "left" ] = { 0, 0, 0 }, 38 | [ "backspace" ] = { 0, 0, 0 }, 39 | [ "delete" ] = { 0, 0, 0 }, 40 | [ "tab" ] = { 0, 0, 0 }, 41 | [ "return" ] = { 0, 0, 0 }, 42 | [ "kpenter" ] = { 0, 0, 0 } 43 | } 44 | 45 | color_themes.dark = 46 | { 47 | text = { 0.8, 0.8, 0.8 }, 48 | tooltip_bg = { 0, 0, 0 }, 49 | tooltip_border = { 0.3, 0.3, 0.3 }, 50 | window_bg = { 0.26, 0.26, 0.26 }, 51 | window_border = { 0, 0, 0 }, 52 | window_titlebar = { 0.08, 0.08, 0.08 }, 53 | window_titlebar_active = { 0, 0, 0 }, 54 | button_bg = { 0.14, 0.14, 0.14 }, 55 | button_bg_hover = { 0.19, 0.19, 0.19 }, 56 | button_bg_click = { 0.12, 0.12, 0.12 }, 57 | button_border = { 0, 0, 0 }, 58 | check_border = { 0, 0, 0 }, 59 | check_border_hover = { 0.5, 0.5, 0.5 }, 60 | check_mark = { 0.3, 0.3, 1 }, 61 | toggle_border = { 0, 0, 0 }, 62 | toggle_border_hover = { 0.5, 0.5, 0.5 }, 63 | toggle_handle = { 0.8, 0.8, 0.8 }, 64 | toggle_bg_off = { 0.3, 0.3, 0.3 }, 65 | toggle_bg_on = { 0.3, 0.3, 1 }, 66 | radio_border = { 0, 0, 0 }, 67 | radio_border_hover = { 0.5, 0.5, 0.5 }, 68 | radio_mark = { 0.3, 0.3, 1 }, 69 | slider_bg = { 0.3, 0.3, 1 }, 70 | slider_bg_hover = { 0.38, 0.38, 1 }, 71 | slider_thumb = { 0.2, 0.2, 1 }, 72 | list_bg = { 0.14, 0.14, 0.14 }, 73 | list_border = { 0, 0, 0 }, 74 | list_selected = { 0.3, 0.3, 1 }, 75 | list_highlight = { 0.3, 0.3, 0.3 }, 76 | list_track = { 0.08, 0.08, 0.08 }, 77 | list_thumb = { 0.36, 0.36, 0.36 }, 78 | list_thumb_hover = { 0.42, 0.42, 0.42 }, 79 | list_thumb_click = { 0.24, 0.24, 0.24 }, 80 | list_button = { 0.8, 0.8, 0.8 }, 81 | list_button_hover = { 1, 1, 1 }, 82 | list_button_click = { 0.5, 0.5, 0.5 }, 83 | textbox_bg = { 0.03, 0.03, 0.03 }, 84 | textbox_bg_hover = { 0.11, 0.11, 0.11 }, 85 | textbox_border = { 0.1, 0.1, 0.1 }, 86 | textbox_border_focused = { 0.58, 0.58, 1 }, 87 | image_button_border_highlight = { 0.5, 0.5, 0.5 }, 88 | tab_bar_bg = { 0.1, 0.1, 0.1 }, 89 | tab_bar_border = { 0, 0, 0 }, 90 | tab_bar_hover = { 0.2, 0.2, 0.2 }, 91 | tab_bar_highlight = { 0.3, 0.3, 1 }, 92 | progress_bar_bg = { 0.2, 0.2, 0.2 }, 93 | progress_bar_fill = { 0.3, 0.3, 1 }, 94 | progress_bar_border = { 0, 0, 0 }, 95 | modal_tint = { 0.3, 0.3, 0.3 }, 96 | separator = { 0, 0, 0 } 97 | } 98 | 99 | color_themes.light = 100 | { 101 | text = { 0.02, 0.02, 0.02 }, 102 | tooltip_bg = { 1, 1, 1 }, 103 | tooltip_border = { 0, 0, 0 }, 104 | window_bg = { 0.930, 0.930, 0.930 }, 105 | window_border = { 0.000, 0.000, 0.000 }, 106 | window_titlebar = { 0.8, 0.8, 0.8 }, 107 | window_titlebar_active = { 0.54, 0.54, 0.54 }, 108 | button_bg = { 0.800, 0.800, 0.800 }, 109 | button_bg_hover = { 0.900, 0.900, 0.900 }, 110 | button_bg_click = { 0.120, 0.120, 0.120 }, 111 | button_border = { 0.000, 0.000, 0.000 }, 112 | check_border = { 0.000, 0.000, 0.000 }, 113 | check_border_hover = { 0.760, 0.760, 0.760 }, 114 | check_mark = { 0.000, 0.000, 0.000 }, 115 | toggle_border = { 0, 0, 0 }, 116 | toggle_border_hover = { 1, 1, 1 }, 117 | toggle_handle = { 1, 1, 1 }, 118 | toggle_bg_off = { 0.4, 0.4, 0.4 }, 119 | toggle_bg_on = { 0.830, 0.830, 0.830 }, 120 | radio_border = { 0.000, 0.000, 0.000 }, 121 | radio_border_hover = { 0.760, 0.760, 0.760 }, 122 | radio_mark = { 0.172, 0.172, 0.172 }, 123 | slider_bg = { 0.830, 0.830, 0.830 }, 124 | slider_bg_hover = { 0.870, 0.870, 0.870 }, 125 | slider_thumb = { 0.700, 0.700, 0.700 }, 126 | list_bg = { 0.9, 0.9, 0.9 }, 127 | list_border = { 0.000, 0.000, 0.000 }, 128 | list_selected = { 0.686, 0.687, 0.688 }, 129 | list_highlight = { 0.808, 0.810, 0.811 }, 130 | list_track = { 0.82, 0.82, 0.82 }, 131 | list_thumb = { 0.65, 0.65, 0.65 }, 132 | list_thumb_hover = { 0.72, 0.72, 0.72 }, 133 | list_thumb_click = { 0.58, 0.58, 0.58 }, 134 | list_button = { 0, 0, 0 }, 135 | list_button_hover = { 0.3, 0.3, 0.3 }, 136 | list_button_click = { 0.1, 0.1, 0.1 }, 137 | textbox_bg = { 0.700, 0.700, 0.700 }, 138 | textbox_bg_hover = { 0.570, 0.570, 0.570 }, 139 | textbox_border = { 0.000, 0.000, 0.000 }, 140 | textbox_border_focused = { 0.000, 0.000, 1.000 }, 141 | image_button_border_highlight = { 0.500, 0.500, 0.500 }, 142 | tab_bar_bg = { 1.000, 0.994, 0.999 }, 143 | tab_bar_border = { 0.000, 0.000, 0.000 }, 144 | tab_bar_hover = { 0.802, 0.797, 0.795 }, 145 | tab_bar_highlight = { 0.151, 0.140, 1.000 }, 146 | progress_bar_bg = { 1.000, 1.000, 1.000 }, 147 | progress_bar_fill = { 0.830, 0.830, 1.000 }, 148 | progress_bar_border = { 0.000, 0.000, 0.000 }, 149 | modal_tint = { 0.15, 0.15, 0.15 }, 150 | separator = { 0.5, 0.5, 0.5 } 151 | } 152 | 153 | local colors = color_themes.dark 154 | 155 | -- -------------------------------------------------------------------------- -- 156 | -- Framework -- 157 | -- -------------------------------------------------------------------------- -- 158 | 159 | -- LOVR implementation 160 | function framework.GetKeyDown_LOVR( key ) 161 | return lovr.system.isKeyDown( key ) 162 | end 163 | 164 | function framework.NewSampler_LOVR() 165 | return lovr.graphics.newSampler( { wrap = 'clamp' } ) 166 | end 167 | 168 | function framework.LoadFont_LOVR( lib_path, size ) 169 | return lovr.graphics.newFont( lib_path .. "DejaVuSansMono.ttf", size or 14, 4 ) 170 | end 171 | 172 | function framework.SetPixelDensity_LOVR( handle ) 173 | handle:setPixelDensity( 1.0 ) 174 | end 175 | 176 | function framework.SetKeyRepeat_LOVR() 177 | lovr.system.setKeyRepeat( true ) 178 | end 179 | 180 | function framework.IsMouseDown_LOVR( btn ) 181 | return lovr.system.isMouseDown( btn ) 182 | end 183 | 184 | function framework.GetMousePosition_LOVR() 185 | return lovr.system.getMousePosition() 186 | end 187 | 188 | function framework.GetWindowDimensions_LOVR() 189 | return lovr.system.getWindowDimensions() 190 | end 191 | 192 | function framework.GetTime_LOVR() 193 | return lovr.timer.getTime() 194 | end 195 | 196 | function framework.NewTexture_LOVR( w, h ) 197 | return lovr.graphics.newTexture( w, h, texture_flags ) 198 | end 199 | 200 | function framework.SetCanvas_LOVR( pass, tex ) 201 | if not pass then return end 202 | pass:setCanvas( tex ) 203 | end 204 | 205 | function framework.NewPass_LOVR( tex ) 206 | return lovr.graphics.newPass( tex ) 207 | end 208 | 209 | function framework.SetFont_LOVR( pass ) 210 | pass:setFont( font.handle ) 211 | end 212 | 213 | function framework.ResetPass_LOVR( pass ) 214 | pass:reset() 215 | end 216 | 217 | function framework.ClearWindow_LOVR( win ) 218 | win.pass:setDepthTest( nil ) 219 | win.pass:setProjection( 1, mat4():orthographic( win.pass:getDimensions() ) ) 220 | win.pass:setColor( colors.window_bg ) 221 | win.pass:fill() 222 | end 223 | 224 | function framework.SetColor_LOVR( pass, color ) 225 | pass:setColor( color ) 226 | end 227 | 228 | function framework.DrawRect_LOVR( pass, x, y, w, h, type ) 229 | pass:plane( x, y, 0, w, h, 0, 0, 0, 0, type ) 230 | end 231 | 232 | function framework.DrawCircle_LOVR( pass, x, y, radius, type ) 233 | pass:circle( x, y, 0, radius, 0, 0, 0, 0, type ) 234 | end 235 | 236 | function framework.DrawCircleHalf_LOVR( pass, x, y, radius, type, angle1, angle2 ) 237 | pass:circle( x, y, 0, radius, 0, 0, 0, 0, type, angle1, angle2 ) 238 | end 239 | 240 | function framework.DrawLine_LOVR( pass, x1, y1, x2, y2 ) 241 | pass:line( x1, y1, 0, x2, y2, 0 ) 242 | end 243 | 244 | function framework.DrawText_LOVR( pass, text, x, y, w, h, text_w ) 245 | pass:text( text, x + (w / 2), y + (h / 2), 0 ) 246 | end 247 | 248 | function framework.DrawImage_LOVR( pass, tex, x, y, w, h, sampler ) 249 | pass:setMaterial( tex ) 250 | pass:setSampler( sampler ) 251 | pass:plane( x, y, 0, w, -h ) 252 | pass:setMaterial() 253 | pass:setColor( 1, 1, 1 ) 254 | end 255 | 256 | function framework.SetProjection_LOVR( pass ) 257 | pass:setProjection( 1, mat4():orthographic( pass:getDimensions() ) ) 258 | end 259 | 260 | function framework.ReleaseTexture_LOVR( tex ) 261 | -- noop 262 | end 263 | 264 | function framework.SetMaterial_LOVR( pass, tex ) 265 | pass:setMaterial( tex ) 266 | end 267 | 268 | -- LOVE implementation 269 | function framework.GetKeyDown_LOVE( key ) 270 | return love.keyboard.isDown( key ) 271 | end 272 | 273 | function framework.NewSampler_LOVE() 274 | -- noop 275 | end 276 | 277 | function framework.LoadFont_LOVE( lib_path, size ) 278 | return love.graphics.newFont( lib_path .. "DejaVuSansMono.ttf", size or 14 ) 279 | end 280 | 281 | function framework.SetPixelDensity_LOVE( handle ) 282 | -- noop 283 | end 284 | 285 | function framework.SetKeyRepeat_LOVE() 286 | love.keyboard.setKeyRepeat( true ) 287 | end 288 | 289 | function framework.IsMouseDown_LOVE( btn ) 290 | return love.mouse.isDown( btn ) 291 | end 292 | 293 | function framework.GetMousePosition_LOVE() 294 | return love.mouse.getPosition() 295 | end 296 | 297 | function framework.GetWindowDimensions_LOVE() 298 | return love.window.getMode() 299 | end 300 | 301 | function framework.GetTime_LOVE() 302 | return love.timer.getTime() 303 | end 304 | 305 | function framework.NewTexture_LOVE( w, h ) 306 | return love.graphics.newCanvas( w, h ) 307 | end 308 | 309 | function framework.SetCanvas_LOVE( pass, tex ) 310 | if not tex then 311 | love.graphics.setCanvas() 312 | end 313 | love.graphics.setCanvas( tex ) 314 | end 315 | 316 | function framework.NewPass_LOVE( tex ) 317 | -- noop 318 | end 319 | 320 | function framework.SetFont_LOVE( pass ) 321 | love.graphics.setFont( font.handle ) 322 | end 323 | 324 | function framework.ResetPass_LOVE( pass ) 325 | -- noop 326 | end 327 | 328 | function framework.ClearWindow_LOVE( win ) 329 | love.graphics.clear( colors.window_bg ) 330 | end 331 | 332 | function framework.SetColor_LOVE( pass, color ) 333 | love.graphics.setColor( color ) 334 | end 335 | 336 | function framework.DrawRect_LOVE( pass, x, y, w, h, type ) 337 | love.graphics.rectangle( type, x - (w / 2), y - (h / 2), w, h ) 338 | end 339 | 340 | function framework.DrawCircle_LOVE( pass, x, y, radius, type ) 341 | love.graphics.circle( type, x, y, radius ) 342 | end 343 | 344 | function framework.DrawCircleHalf_LOVE( pass, x, y, radius, type, angle1, angle2 ) 345 | love.graphics.arc( type, "open", x, y, radius, angle1, angle2 ) 346 | end 347 | 348 | function framework.DrawLine_LOVE( pass, x1, y1, x2, y2 ) 349 | love.graphics.line( x1, y1, x2, y2 ) 350 | end 351 | 352 | function framework.DrawText_LOVE( pass, text, x, y, w, h, text_w ) 353 | local posx = (x + (w - text_w) / 2) 354 | local posy = (y + (h - font.h) / 2) 355 | 356 | love.graphics.print( text, posx, posy ) 357 | end 358 | 359 | function framework.DrawImage_LOVE( pass, tex, x, y, w, h, sampler, image_w, image_h ) 360 | love.graphics.draw( tex, x - (w / 2), y - (h / 2), 0, w / image_w, h / image_h ) 361 | end 362 | 363 | function framework.SetProjection_LOVE( pass ) 364 | -- noop 365 | end 366 | 367 | function framework.ReleaseTexture_LOVE( tex ) 368 | tex:release() 369 | end 370 | 371 | function framework.SetMaterial_LOVE( pass, tex ) 372 | -- noop 373 | end 374 | 375 | -- -------------------------------------------------------------------------- -- 376 | -- Internals -- 377 | -- -------------------------------------------------------------------------- -- 378 | local function Clamp( n, n_min, n_max ) 379 | if n < n_min then 380 | n = n_min 381 | elseif n > n_max then 382 | n = n_max 383 | end 384 | 385 | return n 386 | end 387 | 388 | local function GetLineCount( str ) 389 | -- https://stackoverflow.com/questions/24690910/how-to-get-lines-count-in-string/70137660#70137660 390 | local lines = 1 391 | for i = 1, #str do 392 | local c = str:sub( i, i ) 393 | if c == '\n' then lines = lines + 1 end 394 | end 395 | 396 | return lines 397 | end 398 | 399 | local function WindowExists( id ) 400 | for i, v in ipairs( windows ) do 401 | if v.id == id then 402 | return true, i 403 | end 404 | end 405 | return false, 0 406 | end 407 | 408 | local function WidgetExists( win, id ) 409 | for i, v in ipairs( win.cw ) do 410 | if v.id == id then 411 | return true, i 412 | end 413 | end 414 | return false, 0 415 | end 416 | 417 | local function ListBoxExists( id ) 418 | for i, v in ipairs( listbox_state ) do 419 | if v.id == id then 420 | return true, i 421 | end 422 | end 423 | return false, 0 424 | end 425 | 426 | local function PointInRect( px, py, rx, ry, rw, rh ) 427 | if px >= rx and px <= rx + rw and py >= ry and py <= ry + rh then 428 | return true 429 | end 430 | 431 | return false 432 | end 433 | 434 | local function MapRange( from_min, from_max, to_min, to_max, v ) 435 | return (v - from_min) * (to_max - to_min) / (from_max - from_min) + to_min 436 | end 437 | 438 | local function GetLabelPart( name ) 439 | local i = string.find( name, "##" ) 440 | if i then 441 | return string.sub( name, 1, i - 1 ) 442 | end 443 | return name 444 | end 445 | 446 | local function GetLongerStringLen( t ) 447 | local len = 0 448 | local idx = 0 449 | for i, v in ipairs( t ) do 450 | local cur = utf8.len( v ) 451 | if cur > len then 452 | len = cur 453 | idx = i 454 | end 455 | end 456 | 457 | return len 458 | end 459 | 460 | local function ResetLayout() 461 | layout = { x = 0, y = 0, w = 0, h = 0, row_h = 0, total_w = 0, total_h = 0, same_line = false, same_column = false } 462 | end 463 | 464 | local function UpdateLayout( bbox ) 465 | -- Update row height 466 | if layout.same_line then 467 | if bbox.h > layout.row_h then 468 | layout.row_h = bbox.h 469 | end 470 | elseif layout.same_column then 471 | if bbox.h + layout.h + margin < layout.row_h then 472 | layout.row_h = layout.row_h - layout.h - margin 473 | else 474 | layout.row_h = bbox.h 475 | end 476 | else 477 | layout.row_h = bbox.h 478 | end 479 | 480 | -- Calculate current layout w/h 481 | if bbox.x + bbox.w + margin > layout.total_w then 482 | layout.total_w = bbox.x + bbox.w + margin 483 | end 484 | 485 | if bbox.y + layout.row_h + margin > layout.total_h then 486 | layout.total_h = bbox.y + layout.row_h + margin 487 | end 488 | 489 | -- Update layout x/y/w/h and same_line 490 | layout.x = bbox.x 491 | layout.y = bbox.y 492 | layout.w = bbox.w 493 | layout.h = bbox.h 494 | layout.same_line = false 495 | layout.same_column = false 496 | end 497 | 498 | local function Slider( type, name, v, v_min, v_max, width, num_decimals, tooltip ) 499 | local text = GetLabelPart( name ) 500 | local cur_window = windows[ begin_idx ] 501 | local text_w = font.handle:getWidth( text ) 502 | 503 | local slider_w = 10 * font.w 504 | local bbox = {} 505 | if layout.same_line then 506 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = slider_w + margin + text_w, h = (2 * margin) + font.h } 507 | elseif layout.same_column then 508 | bbox = { x = layout.x, y = layout.y + layout.h + margin, w = slider_w + margin + text_w, h = (2 * margin) + font.h } 509 | else 510 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = slider_w + margin + text_w, h = (2 * margin) + font.h } 511 | end 512 | 513 | if width and width > bbox.w then 514 | bbox.w = width 515 | slider_w = width - margin - text_w 516 | end 517 | 518 | UpdateLayout( bbox ) 519 | 520 | local col = colors.slider_bg 521 | local result = false 522 | 523 | if not modal_window or (modal_window and modal_window == cur_window) then 524 | if PointInRect( mouse.x, mouse.y, bbox.x + cur_window.x, bbox.y + cur_window.y, slider_w, bbox.h ) and cur_window == active_window then 525 | if tooltip then 526 | active_tooltip.text = tooltip 527 | active_tooltip.x = mouse.x 528 | active_tooltip.y = mouse.y 529 | end 530 | col = colors.slider_bg_hover 531 | 532 | if mouse.state == e_mouse_state.clicked then 533 | active_widget = cur_window.id .. name 534 | end 535 | end 536 | end 537 | 538 | if mouse.state == e_mouse_state.held and active_widget == cur_window.id .. name and cur_window == active_window then 539 | v = MapRange( bbox.x + 2, bbox.x + slider_w - 2, v_min, v_max, mouse.x - cur_window.x ) 540 | if type == e_slider_type.float then 541 | v = Clamp( v, v_min, v_max ) 542 | else 543 | v = Clamp( math.ceil( v ), v_min, v_max ) 544 | if v == 0 then v = 0 end 545 | end 546 | end 547 | if mouse.state == e_mouse_state.released and active_widget == cur_window.id .. name then 548 | active_widget = nil 549 | result = true 550 | end 551 | 552 | local value_text_w = font.handle:getWidth( v ) 553 | local text_label_rect = { x = bbox.x + slider_w + margin, y = bbox.y, w = text_w, h = bbox.h } 554 | local text_value_rect = { x = bbox.x, y = bbox.y, w = slider_w, h = bbox.h } 555 | local slider_rect = { x = bbox.x, y = bbox.y + (bbox.h / 2) - (font.h / 2), w = slider_w, h = font.h } 556 | local thumb_pos = MapRange( v_min, v_max, bbox.x, bbox.x + slider_w - font.h, v ) 557 | local thumb_rect = { x = thumb_pos, y = bbox.y + (bbox.h / 2) - (font.h / 2), w = font.h, h = font.h } 558 | 559 | local value 560 | if type == e_slider_type.float then 561 | num_decimals = num_decimals or 2 562 | local str_fmt = "%." .. num_decimals .. "f" 563 | value = string.format( str_fmt, v ) 564 | else 565 | value = v 566 | end 567 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = slider_rect, color = col } ) 568 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = thumb_rect, color = colors.slider_thumb } ) 569 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = text, bbox = text_label_rect, color = colors.text } ) 570 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = value, bbox = text_value_rect, color = colors.text } ) 571 | return v, result 572 | end 573 | 574 | function utf8.sub( s, i, j ) 575 | i = utf8.offset( s, i ) or 1 576 | local nextOffset = utf8.offset( s, j + 1 ) 577 | j = (nextOffset and nextOffset - 1) or #tostring( s ) 578 | return string.sub( s, i, j ) 579 | end 580 | 581 | -- -------------------------------------------------------------------------- -- 582 | -- User -- 583 | -- -------------------------------------------------------------------------- -- 584 | function UI2D.KeyPressed( key, repeating ) 585 | if repeating then 586 | if key == "right" then 587 | repeating_key = "right" 588 | elseif key == "left" then 589 | repeating_key = "left" 590 | elseif key == "backspace" then 591 | repeating_key = "backspace" 592 | elseif key == "delete" then 593 | repeating_key = "delete" 594 | end 595 | end 596 | end 597 | 598 | function UI2D.TextInput( text ) 599 | text_input_character = text 600 | end 601 | 602 | function UI2D.KeyReleased() 603 | repeating_key = nil 604 | end 605 | 606 | function UI2D.WheelMoved( x, y ) 607 | mouse.wheel_x = x 608 | mouse.wheel_y = y 609 | end 610 | 611 | function UI2D.Init( type, size ) 612 | framework.type = type 613 | if type == "lovr" then 614 | framework.GetKeyDown = framework.GetKeyDown_LOVR 615 | framework.NewSampler = framework.NewSampler_LOVR 616 | framework.LoadFont = framework.LoadFont_LOVR 617 | framework.SetPixelDensity = framework.SetPixelDensity_LOVR 618 | framework.SetKeyRepeat = framework.SetKeyRepeat_LOVR 619 | framework.IsMouseDown = framework.IsMouseDown_LOVR 620 | framework.GetMousePosition = framework.GetMousePosition_LOVR 621 | framework.GetWindowDimensions = framework.GetWindowDimensions_LOVR 622 | framework.GetTime = framework.GetTime_LOVR 623 | framework.NewTexture = framework.NewTexture_LOVR 624 | framework.SetCanvas = framework.SetCanvas_LOVR 625 | framework.NewPass = framework.NewPass_LOVR 626 | framework.SetFont = framework.SetFont_LOVR 627 | framework.ResetPass = framework.ResetPass_LOVR 628 | framework.ClearWindow = framework.ClearWindow_LOVR 629 | framework.SetColor = framework.SetColor_LOVR 630 | framework.DrawRect = framework.DrawRect_LOVR 631 | framework.DrawCircle = framework.DrawCircle_LOVR 632 | framework.DrawText = framework.DrawText_LOVR 633 | framework.DrawImage = framework.DrawImage_LOVR 634 | framework.SetProjection = framework.SetProjection_LOVR 635 | framework.ReleaseTexture = framework.ReleaseTexture_LOVR 636 | framework.SetMaterial = framework.SetMaterial_LOVR 637 | framework.DrawCircleHalf = framework.DrawCircleHalf_LOVR 638 | framework.DrawLine = framework.DrawLine_LOVR 639 | else 640 | framework.GetKeyDown = framework.GetKeyDown_LOVE 641 | framework.NewSampler = framework.NewSampler_LOVE 642 | framework.LoadFont = framework.LoadFont_LOVE 643 | framework.SetPixelDensity = framework.SetPixelDensity_LOVE 644 | framework.SetKeyRepeat = framework.SetKeyRepeat_LOVE 645 | framework.IsMouseDown = framework.IsMouseDown_LOVE 646 | framework.GetMousePosition = framework.GetMousePosition_LOVE 647 | framework.GetWindowDimensions = framework.GetWindowDimensions_LOVE 648 | framework.GetTime = framework.GetTime_LOVE 649 | framework.NewTexture = framework.NewTexture_LOVE 650 | framework.SetCanvas = framework.SetCanvas_LOVE 651 | framework.NewPass = framework.NewPass_LOVE 652 | framework.SetFont = framework.SetFont_LOVE 653 | framework.ResetPass = framework.ResetPass_LOVE 654 | framework.ClearWindow = framework.ClearWindow_LOVE 655 | framework.SetColor = framework.SetColor_LOVE 656 | framework.DrawRect = framework.DrawRect_LOVE 657 | framework.DrawCircle = framework.DrawCircle_LOVE 658 | framework.DrawText = framework.DrawText_LOVE 659 | framework.DrawImage = framework.DrawImage_LOVE 660 | framework.SetProjection = framework.SetProjection_LOVE 661 | framework.ReleaseTexture = framework.ReleaseTexture_LOVE 662 | framework.SetMaterial = framework.SetMaterial_LOVE 663 | framework.DrawCircleHalf = framework.DrawCircleHalf_LOVE 664 | framework.DrawLine = framework.DrawLine_LOVE 665 | end 666 | 667 | local info = debug.getinfo( 1, "S" ) 668 | local lib_path = info.source:match( "@(.*[\\/])" ) 669 | font.handle = framework.LoadFont( lib_path, size ) 670 | 671 | framework.SetPixelDensity( font.handle ) 672 | font.h = font.handle:getHeight() 673 | font.w = font.handle:getWidth( "W" ) 674 | font.size = size or 14 675 | framework.SetKeyRepeat() 676 | 677 | margin = math.floor( font.h / 2 ) 678 | separator_thickness = math.floor( font.h / 7 ) 679 | end 680 | 681 | function UI2D.InputInfo() 682 | for i, v in pairs( keys ) do 683 | if framework.GetKeyDown( i ) then 684 | if v[ 1 ] == 0 then 685 | v[ 1 ] = 1 686 | v[ 2 ] = 1 687 | v[ 3 ] = 1 -- pressed 688 | else 689 | v[ 1 ] = 1 690 | v[ 2 ] = 0 691 | v[ 3 ] = 2 -- held 692 | end 693 | else 694 | if v[ 1 ] == 1 then 695 | v[ 1 ] = 0 696 | v[ 3 ] = 3 -- released 697 | else 698 | v[ 1 ] = 0 699 | v[ 1 ] = 0 700 | v[ 3 ] = 0 -- idle 701 | end 702 | end 703 | end 704 | 705 | if framework.IsMouseDown( 1 ) then 706 | if mouse.prev_frame == 0 then 707 | mouse.prev_frame = 1 708 | mouse.this_frame = 1 709 | mouse.state = e_mouse_state.clicked 710 | else 711 | mouse.prev_frame = 1 712 | mouse.this_frame = 0 713 | mouse.state = e_mouse_state.held 714 | end 715 | else 716 | if mouse.prev_frame == 1 then 717 | mouse.state = e_mouse_state.released 718 | mouse.prev_frame = 0 719 | else 720 | mouse.state = e_mouse_state.idle 721 | end 722 | end 723 | 724 | mouse.x, mouse.y = framework.GetMousePosition() 725 | 726 | -- Set active window on click 727 | local hovers_active = false 728 | local hovers_any = false 729 | for i, v in ipairs( windows ) do 730 | if PointInRect( mouse.x, mouse.y, v.x, v.y, v.w, v.h ) then 731 | if v == active_window then 732 | hovers_active = true 733 | end 734 | hovers_any = true 735 | has_mouse = true 736 | end 737 | end 738 | 739 | if modal_window then 740 | active_window = modal_window 741 | hovers_active = false 742 | end 743 | 744 | local z = 0 745 | local win = nil 746 | if not hovers_active then 747 | for i, v in ipairs( windows ) do 748 | if PointInRect( mouse.x, mouse.y, v.x, v.y, v.w, v.h ) and mouse.state == e_mouse_state.clicked then 749 | if v.z > z then 750 | win = v 751 | z = v.z 752 | end 753 | end 754 | end 755 | 756 | if win and not modal_window then 757 | next_z = next_z + 0.01 758 | win.z = next_z 759 | active_window = win 760 | end 761 | end 762 | 763 | -- Set active to none 764 | if not hovers_any and mouse.state == e_mouse_state.clicked then 765 | active_window = nil 766 | has_text_input = false 767 | end 768 | 769 | -- Give back mouse 770 | if not hovers_any then 771 | has_mouse = false 772 | end 773 | 774 | -- Handle window dragging 775 | if active_window then 776 | local v = active_window 777 | if PointInRect( mouse.x, mouse.y, v.x, v.y, v.w, (2 * margin) + font.h ) and mouse.state == e_mouse_state.clicked then 778 | dragged_window = active_window 779 | dragged_window_offset.x = mouse.x - active_window.x 780 | dragged_window_offset.y = mouse.y - active_window.y 781 | end 782 | 783 | if dragged_window then 784 | if mouse.state == e_mouse_state.held then 785 | local mx = mouse.x 786 | local my = mouse.y 787 | local w, h = framework.GetWindowDimensions() 788 | mx = Clamp( mx, 10, w - 10 ) 789 | my = Clamp( my, 10, h - 10 ) 790 | dragged_window.x = mx - dragged_window_offset.x 791 | dragged_window.y = my - dragged_window_offset.y 792 | end 793 | end 794 | end 795 | 796 | if mouse.state == e_mouse_state.released then 797 | dragged_window = nil 798 | end 799 | 800 | local now = framework.GetTime() 801 | if now > caret_blink.prev + 0.4 then 802 | caret_blink.on = true 803 | end 804 | 805 | if now > caret_blink.prev + 0.8 then 806 | caret_blink.on = false 807 | caret_blink.prev = now 808 | end 809 | end 810 | 811 | function UI2D.Begin( name, x, y, is_modal ) 812 | local exists, idx = WindowExists( name ) -- TODO: Can't currently change window title on runtime 813 | 814 | if not exists then 815 | next_z = next_z + 0.01 816 | local window = { 817 | id = name, 818 | title = GetLabelPart( name ), 819 | x = x, 820 | y = y, 821 | z = next_z, 822 | w = 0, 823 | h = 0, 824 | command_list = {}, 825 | texture = nil, 826 | texture_w = 0, 827 | texture_h = 0, 828 | pass = nil, 829 | is_hovered = false, 830 | is_modal = is_modal or false, 831 | was_called_this_frame = true, 832 | cw = {} 833 | } 834 | table.insert( windows, window ) 835 | 836 | if is_modal then 837 | modal_window = window 838 | end 839 | end 840 | layout.y = (2 * margin) + font.h 841 | 842 | if idx == 0 then 843 | begin_idx = #windows 844 | else 845 | begin_idx = idx 846 | end 847 | 848 | if idx > 0 then 849 | windows[ idx ].was_called_this_frame = true 850 | end 851 | 852 | begin_end_pairs.b = begin_end_pairs.b + 1 853 | end 854 | 855 | function UI2D.End( main_pass ) 856 | local cur_window = windows[ begin_idx ] 857 | cur_window.w = layout.total_w 858 | cur_window.h = layout.total_h 859 | assert( cur_window.w > 0, "Begin/End block without widgets!" ) 860 | 861 | -- Cache texture 862 | if cur_window.texture then 863 | if cur_window.texture_w ~= cur_window.w or cur_window.texture_h ~= cur_window.h then 864 | cur_window.texture:release() 865 | cur_window.texture_w = cur_window.w 866 | cur_window.texture_h = cur_window.h 867 | cur_window.texture = framework.NewTexture( cur_window.w, cur_window.h ) 868 | framework.SetCanvas( cur_window.pass, cur_window.texture ) 869 | end 870 | else 871 | cur_window.texture = framework.NewTexture( cur_window.w, cur_window.h ) 872 | cur_window.texture_w = cur_window.w 873 | cur_window.texture_h = cur_window.h 874 | cur_window.pass = framework.NewPass( cur_window.texture ) 875 | end 876 | 877 | framework.SetCanvas( nil, cur_window.texture ) 878 | framework.ResetPass( cur_window.pass ) 879 | framework.SetFont( cur_window.pass ) 880 | framework.ClearWindow( cur_window ) 881 | 882 | -- Title bar and border 883 | local title_col = colors.window_titlebar 884 | if cur_window == active_window then 885 | title_col = colors.window_titlebar_active 886 | end 887 | table.insert( windows[ begin_idx ].command_list, 888 | { type = "rect_fill", bbox = { x = 0, y = 0, w = cur_window.w, h = (2 * margin) + font.h }, color = title_col } ) 889 | 890 | local txt = cur_window.title 891 | local title_w = utf8.len( txt ) * font.w 892 | if title_w > cur_window.w - (2 * margin) then -- Truncate title 893 | local num_chars = ((cur_window.w - (2 * margin)) / font.w) - 3 894 | txt = string.sub( txt, 1, num_chars ) .. "..." 895 | title_w = utf8.len( txt ) * font.w 896 | end 897 | 898 | table.insert( windows[ begin_idx ].command_list, 899 | { type = "text", text = txt, bbox = { x = margin, y = 0, w = title_w, h = (2 * margin) + font.h }, color = colors.text } ) 900 | 901 | table.insert( windows[ begin_idx ].command_list, 902 | { type = "rect_wire", bbox = { x = 0, y = 0, w = cur_window.w, h = cur_window.h }, color = colors.window_border } ) 903 | 904 | -- Do draw commands 905 | for i, v in ipairs( cur_window.command_list ) do 906 | if v.type == "rect_fill" then 907 | if v.is_separator then 908 | framework.SetColor( cur_window.pass, v.color ) 909 | framework.DrawRect( cur_window.pass, v.bbox.x + (cur_window.w / 2), v.bbox.y, cur_window.w - (2 * margin), separator_thickness, "fill" ) 910 | else 911 | framework.SetColor( cur_window.pass, v.color ) 912 | framework.DrawRect( cur_window.pass, v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), v.bbox.w, v.bbox.h, "fill" ) 913 | end 914 | elseif v.type == "rect_wire" then 915 | framework.SetColor( cur_window.pass, v.color ) 916 | framework.DrawRect( cur_window.pass, v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), v.bbox.w, v.bbox.h, "line" ) 917 | elseif v.type == "circle_wire" then 918 | framework.SetColor( cur_window.pass, v.color ) 919 | framework.DrawCircle( cur_window.pass, v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), v.bbox.w / 2, "line" ) 920 | elseif v.type == "circle_fill" then 921 | framework.SetColor( cur_window.pass, v.color ) 922 | framework.DrawCircle( cur_window.pass, v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), v.bbox.w / 3, "fill" ) 923 | elseif v.type == "circle_wire_half" then 924 | framework.SetColor( cur_window.pass, v.color ) 925 | framework.DrawCircleHalf( cur_window.pass, v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), v.bbox.w / 2, "line", v.angle1, v.angle2 ) 926 | elseif v.type == "circle_fill_half" then 927 | framework.SetColor( cur_window.pass, v.color ) 928 | framework.DrawCircleHalf( cur_window.pass, v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), v.bbox.w / 2, "fill", v.angle1, v.angle2 ) 929 | elseif v.type == "line" then 930 | framework.SetColor( cur_window.pass, v.color ) 931 | framework.DrawLine( cur_window.pass, v.x1, v.y1, v.x2, v.y2 ) 932 | elseif v.type == "text" then 933 | framework.SetColor( cur_window.pass, v.color ) 934 | local text_w = font.handle:getWidth( v.text ) 935 | framework.DrawText( cur_window.pass, v.text, v.bbox.x, v.bbox.y, v.bbox.w, v.bbox.h, text_w ) 936 | elseif v.type == "image" then 937 | -- NOTE Temp fix. Had to do negative vertical scale. Otherwise image gets flipped? 938 | framework.SetColor( cur_window.pass, v.color ) 939 | framework.DrawImage( cur_window.pass, v.texture, v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), v.bbox.w, v.bbox.h, clamp_sampler, v.image_w, v.image_h ) 940 | end 941 | end 942 | 943 | ResetLayout() 944 | begin_end_pairs.e = begin_end_pairs.e + 1 945 | end 946 | 947 | function UI2D.HasMouse() 948 | return has_mouse 949 | end 950 | 951 | function UI2D.SetWindowPosition( name, x, y ) 952 | local exists, idx = WindowExists( name ) 953 | if exists then 954 | windows[ idx ].x = x 955 | windows[ idx ].y = y 956 | return true 957 | end 958 | 959 | return false 960 | end 961 | 962 | function UI2D.GetWindowPosition( name ) 963 | local exists, idx = WindowExists( name ) 964 | if exists then 965 | return windows[ idx ].x, windows[ idx ].y 966 | end 967 | 968 | return nil 969 | end 970 | 971 | function UI2D.GetWindowSize( name ) 972 | local exists, idx = WindowExists( name ) 973 | if exists then 974 | return windows[ idx ].w, windows[ idx ].h 975 | end 976 | 977 | return nil 978 | end 979 | 980 | function UI2D.SetColorTheme( theme, copy_from ) 981 | if type( theme ) == "string" then 982 | colors = color_themes[ theme ] 983 | elseif type( theme ) == "table" then 984 | copy_from = copy_from or "dark" 985 | for i, v in pairs( color_themes[ copy_from ] ) do 986 | if theme[ i ] == nil then 987 | theme[ i ] = v 988 | end 989 | end 990 | colors = theme 991 | end 992 | end 993 | 994 | function UI2D.GetColorTheme() 995 | for i, v in pairs( color_themes ) do 996 | if v == colors then 997 | return i 998 | end 999 | end 1000 | end 1001 | 1002 | function UI2D.OverrideColor( col_name, color ) 1003 | if not overriden_colors[ col_name ] then 1004 | local old_color = colors[ col_name ] 1005 | overriden_colors[ col_name ] = old_color 1006 | colors[ col_name ] = color 1007 | end 1008 | end 1009 | 1010 | function UI2D.ResetColor( col_name ) 1011 | if overriden_colors[ col_name ] then 1012 | colors[ col_name ] = overriden_colors[ col_name ] 1013 | overriden_colors[ col_name ] = nil 1014 | end 1015 | end 1016 | 1017 | function UI2D.SetFontSize( size ) 1018 | local info = debug.getinfo( 1, "S" ) 1019 | local lib_path = info.source:match( "@(.*[\\/])" ) 1020 | 1021 | clamp_sampler = framework.NewSampler() 1022 | local lib_path = info.source:match( "@(.*[\\/])" ) 1023 | font.handle = framework.LoadFont( lib_path, size ) 1024 | 1025 | framework.SetPixelDensity( font.handle ) 1026 | font.h = font.handle:getHeight() 1027 | font.w = font.handle:getWidth( "W" ) 1028 | font.size = size 1029 | 1030 | margin = math.floor( font.h / 2 ) 1031 | separator_thickness = math.floor( font.h / 7 ) 1032 | end 1033 | 1034 | function UI2D.GetFontSize() 1035 | return font.size 1036 | end 1037 | 1038 | function UI2D.HasTextInput() 1039 | return has_text_input 1040 | end 1041 | 1042 | function UI2D.IsModalOpen() 1043 | return modal_window 1044 | end 1045 | 1046 | function UI2D.EndModalWindow() 1047 | modal_window = nil 1048 | end 1049 | 1050 | function UI2D.SameLine() 1051 | layout.same_line = true 1052 | end 1053 | 1054 | function UI2D.SameColumn() 1055 | layout.same_column = true 1056 | end 1057 | 1058 | function UI2D.Button( name, width, height, tooltip ) 1059 | local text = GetLabelPart( name ) 1060 | local cur_window = windows[ begin_idx ] 1061 | local text_w = utf8.len( text ) * font.w 1062 | local num_lines = GetLineCount( text ) 1063 | 1064 | local bbox = {} 1065 | if layout.same_line then 1066 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = (2 * margin) + text_w, h = (2 * margin) + (num_lines * font.h) } 1067 | elseif layout.same_column then 1068 | bbox = { x = layout.x, y = layout.y + layout.h + margin, w = (2 * margin) + text_w, h = (2 * margin) + (num_lines * font.h) } 1069 | else 1070 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = (2 * margin) + text_w, h = (2 * margin) + (num_lines * font.h) } 1071 | end 1072 | 1073 | if width and type( width ) == "number" and width > bbox.w then 1074 | bbox.w = width 1075 | end 1076 | if height and type( height ) == "number" and height > bbox.h then 1077 | bbox.h = height 1078 | end 1079 | 1080 | UpdateLayout( bbox ) 1081 | 1082 | local result = false 1083 | local col = colors.button_bg 1084 | 1085 | if not modal_window or (modal_window and modal_window == cur_window) then 1086 | if PointInRect( mouse.x, mouse.y, bbox.x + cur_window.x, bbox.y + cur_window.y, bbox.w, bbox.h ) and cur_window == active_window then 1087 | if tooltip then 1088 | active_tooltip.text = tooltip 1089 | active_tooltip.x = mouse.x 1090 | active_tooltip.y = mouse.y 1091 | end 1092 | col = colors.button_bg_hover 1093 | if mouse.state == e_mouse_state.clicked then 1094 | active_widget = cur_window.id .. name 1095 | end 1096 | if mouse.state == e_mouse_state.held then 1097 | col = colors.button_bg_click 1098 | end 1099 | if mouse.state == e_mouse_state.released and active_widget == cur_window.id .. name then 1100 | active_widget = nil 1101 | result = true 1102 | end 1103 | end 1104 | end 1105 | 1106 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = bbox, color = col } ) 1107 | table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = bbox, color = colors.button_border } ) 1108 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = text, bbox = bbox, color = colors.text } ) 1109 | 1110 | return result 1111 | end 1112 | 1113 | function UI2D.SliderInt( name, v, v_min, v_max, width, tooltip ) 1114 | return Slider( e_slider_type.int, name, v, v_min, v_max, width, tooltip ) 1115 | end 1116 | 1117 | function UI2D.SliderFloat( name, v, v_min, v_max, width, num_decimals, tooltip ) 1118 | return Slider( e_slider_type.float, name, v, v_min, v_max, width, num_decimals, tooltip ) 1119 | end 1120 | 1121 | function UI2D.ProgressBar( progress, width, tooltip ) 1122 | local cur_window = windows[ begin_idx ] 1123 | if width and width >= (2 * margin) + (4 * font.w) then 1124 | width = width 1125 | else 1126 | width = 300 1127 | end 1128 | 1129 | local bbox = {} 1130 | if layout.same_line then 1131 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = width, h = (2 * margin) + font.h } 1132 | elseif layout.same_column then 1133 | bbox = { x = layout.x, y = layout.y + layout.h, w = width, h = (2 * margin) + font.h } 1134 | else 1135 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = width, h = (2 * margin) + font.h } 1136 | end 1137 | 1138 | UpdateLayout( bbox ) 1139 | 1140 | if not modal_window or (modal_window and modal_window == cur_window) then 1141 | if PointInRect( mouse.x, mouse.y, bbox.x + cur_window.x, bbox.y + cur_window.y, bbox.w, bbox.h ) and cur_window == active_window then 1142 | if tooltip then 1143 | active_tooltip.text = tooltip 1144 | active_tooltip.x = mouse.x 1145 | active_tooltip.y = mouse.y 1146 | end 1147 | end 1148 | end 1149 | 1150 | progress = Clamp( progress, 0, 100 ) 1151 | local fill_w = math.floor( (width * progress) / 100 ) 1152 | local str = progress .. "%" 1153 | 1154 | table.insert( windows[ begin_idx ].command_list, 1155 | { type = "rect_fill", bbox = { x = bbox.x, y = bbox.y, w = fill_w, h = bbox.h }, color = colors.progress_bar_fill } ) 1156 | table.insert( windows[ begin_idx ].command_list, 1157 | { type = "rect_fill", bbox = { x = bbox.x + fill_w, y = bbox.y, w = bbox.w - fill_w, h = bbox.h }, color = colors.progress_bar_bg } ) 1158 | table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = bbox, color = colors.progress_bar_border } ) 1159 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = str, bbox = bbox, color = colors.text } ) 1160 | end 1161 | 1162 | function UI2D.Separator() 1163 | local bbox = {} 1164 | if layout.same_line or layout.same_column then 1165 | return 1166 | else 1167 | bbox = { x = 0, y = layout.y + layout.row_h + margin, w = 0, h = 0 } 1168 | end 1169 | 1170 | UpdateLayout( bbox ) 1171 | 1172 | table.insert( windows[ begin_idx ].command_list, { is_separator = true, type = "rect_fill", bbox = bbox, color = colors.separator } ) 1173 | end 1174 | 1175 | function UI2D.ImageButton( texture, width, height, text, tooltip ) 1176 | local cur_window = windows[ begin_idx ] 1177 | local width = width or texture:getWidth() 1178 | local height = height or texture:getHeight() 1179 | 1180 | local bbox = {} 1181 | if layout.same_line then 1182 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = width, h = height } 1183 | elseif layout.same_column then 1184 | bbox = { x = layout.x, y = layout.y + layout.h + margin, w = width, height = height } 1185 | else 1186 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = width, h = height } 1187 | end 1188 | 1189 | local text_w 1190 | 1191 | if text then 1192 | text_w = font.handle:getWidth( text ) 1193 | font.h = font.handle:getHeight() 1194 | 1195 | if font.h > bbox.h then 1196 | bbox.h = font.h 1197 | end 1198 | bbox.w = bbox.w + (2 * margin) + text_w 1199 | end 1200 | 1201 | UpdateLayout( bbox ) 1202 | 1203 | local result = false 1204 | local col = 1 1205 | 1206 | if not modal_window or (modal_window and modal_window == cur_window) then 1207 | if PointInRect( mouse.x, mouse.y, bbox.x + cur_window.x, bbox.y + cur_window.y, bbox.w, bbox.h ) and cur_window == active_window then 1208 | if tooltip then 1209 | active_tooltip.text = tooltip 1210 | active_tooltip.x = mouse.x 1211 | active_tooltip.y = mouse.y 1212 | end 1213 | table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = bbox, color = colors.image_button_border_highlight } ) 1214 | 1215 | if mouse.state == e_mouse_state.clicked then 1216 | active_widget = cur_window.id .. tostring( texture ) 1217 | end 1218 | if mouse.state == e_mouse_state.held then 1219 | col = 0.7 1220 | end 1221 | if mouse.state == e_mouse_state.released and active_widget == cur_window.id .. tostring( texture ) then 1222 | active_widget = nil 1223 | result = true 1224 | end 1225 | end 1226 | end 1227 | 1228 | local original_w = texture:getWidth() 1229 | local original_h = texture:getHeight() 1230 | 1231 | if text then 1232 | table.insert( windows[ begin_idx ].command_list, 1233 | { 1234 | type = "image", 1235 | bbox = { x = bbox.x, y = bbox.y + ((bbox.h - height) / 2), w = width, h = height }, 1236 | texture = texture, 1237 | image_w = original_w, 1238 | image_h = original_h, 1239 | color = { col, col, col } 1240 | } ) 1241 | table.insert( windows[ begin_idx ].command_list, 1242 | { type = "text", text = text, bbox = { x = bbox.x + width, y = bbox.y, w = text_w + (2 * margin), h = bbox.h }, color = colors.text } ) 1243 | else 1244 | table.insert( windows[ begin_idx ].command_list, { type = "image", bbox = bbox, texture = texture, image_w = original_w, image_h = original_h, color = { col, col, col } } ) 1245 | end 1246 | 1247 | return result 1248 | end 1249 | 1250 | function UI2D.Dummy( width, height ) 1251 | local bbox = {} 1252 | if layout.same_line then 1253 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = width, h = height } 1254 | else 1255 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = width, h = height } 1256 | end 1257 | 1258 | UpdateLayout( bbox ) 1259 | end 1260 | 1261 | function UI2D.TabBar( name, tabs, idx, tooltip ) 1262 | local cur_window = windows[ begin_idx ] 1263 | local bbox = {} 1264 | 1265 | if layout.same_line then 1266 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = 0, h = (2 * margin) + font.h } 1267 | elseif layout.same_column then 1268 | bbox = { x = layout.x, y = layout.y + layout.h + margin, w = 0, h = (2 * margin) + font.h } 1269 | else 1270 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = 0, h = (2 * margin) + font.h } 1271 | end 1272 | 1273 | local result = false, idx 1274 | local total_w = 0 1275 | local col = colors.tab_bar_bg 1276 | local x_off = bbox.x 1277 | 1278 | for i, v in ipairs( tabs ) do 1279 | local text_w = font.handle:getWidth( v ) 1280 | local tab_w = text_w + (2 * margin) 1281 | bbox.w = bbox.w + tab_w 1282 | 1283 | if not modal_window or (modal_window and modal_window == cur_window) then 1284 | if PointInRect( mouse.x, mouse.y, x_off + cur_window.x, bbox.y + cur_window.y, tab_w, bbox.h ) and cur_window == active_window then 1285 | if tooltip then 1286 | active_tooltip.text = tooltip 1287 | active_tooltip.x = mouse.x 1288 | active_tooltip.y = mouse.y 1289 | end 1290 | col = colors.tab_bar_hover 1291 | if mouse.state == e_mouse_state.clicked and cur_window.id .. name then 1292 | idx = i 1293 | result = true 1294 | end 1295 | else 1296 | col = colors.tab_bar_bg 1297 | end 1298 | end 1299 | 1300 | local tab_rect = { x = x_off, y = bbox.y, w = tab_w, h = bbox.h } 1301 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = tab_rect, color = col } ) 1302 | table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = tab_rect, color = colors.tab_bar_border } ) 1303 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = v, bbox = tab_rect, color = colors.text } ) 1304 | 1305 | if idx == i then 1306 | local highlight_thickness = math.floor( font.h / 4 ) 1307 | table.insert( windows[ begin_idx ].command_list, 1308 | { 1309 | type = "rect_fill", 1310 | bbox = { x = tab_rect.x + 2, y = tab_rect.y + tab_rect.h - (highlight_thickness), w = tab_rect.w - 4, h = highlight_thickness }, 1311 | color = colors.tab_bar_highlight 1312 | } ) 1313 | end 1314 | x_off = x_off + tab_w 1315 | end 1316 | 1317 | table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = bbox, color = colors.tab_bar_border } ) 1318 | UpdateLayout( bbox ) 1319 | 1320 | return result, idx 1321 | end 1322 | 1323 | function UI2D.Label( text, compact ) 1324 | local text_w = font.handle:getWidth( text ) 1325 | local num_lines = GetLineCount( text ) 1326 | 1327 | local mrg = (2 * margin) 1328 | if compact then 1329 | mrg = 0 1330 | end 1331 | 1332 | local bbox = {} 1333 | if layout.same_line then 1334 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = text_w, h = mrg + (num_lines * font.h) } 1335 | elseif layout.same_column then 1336 | bbox = { x = layout.x, y = layout.y + layout.h + margin, w = text_w, h = mrg + (num_lines * font.h) } 1337 | else 1338 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = text_w, h = mrg + (num_lines * font.h) } 1339 | end 1340 | 1341 | UpdateLayout( bbox ) 1342 | 1343 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = text, bbox = bbox, color = colors.text } ) 1344 | end 1345 | 1346 | function UI2D.CheckBox( text, checked, tooltip ) 1347 | local cur_window = windows[ begin_idx ] 1348 | local text_w = font.handle:getWidth( text ) 1349 | 1350 | local bbox = {} 1351 | if layout.same_line then 1352 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = font.h + margin + text_w, h = (2 * margin) + font.h } 1353 | elseif layout.same_column then 1354 | bbox = { x = layout.x, y = layout.y + layout.h + margin, w = font.h + margin + text_w, h = (2 * margin) + font.h } 1355 | else 1356 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = font.h + margin + text_w, h = (2 * margin) + font.h } 1357 | end 1358 | 1359 | UpdateLayout( bbox ) 1360 | 1361 | local result = false 1362 | local col = colors.check_border 1363 | 1364 | if not modal_window or (modal_window and modal_window == cur_window) then 1365 | if PointInRect( mouse.x, mouse.y, bbox.x + cur_window.x, bbox.y + cur_window.y, bbox.w, bbox.h ) and cur_window == active_window then 1366 | if tooltip then 1367 | active_tooltip.text = tooltip 1368 | active_tooltip.x = mouse.x 1369 | active_tooltip.y = mouse.y 1370 | end 1371 | col = colors.check_border_hover 1372 | if mouse.state == e_mouse_state.clicked then 1373 | active_widget = cur_window.id .. text 1374 | end 1375 | if mouse.state == e_mouse_state.released and active_widget == cur_window.id .. text then 1376 | active_widget = nil 1377 | result = true 1378 | end 1379 | end 1380 | end 1381 | 1382 | local check_rect = { x = bbox.x, y = bbox.y + margin, w = font.h, h = font.h } 1383 | local text_rect = { x = bbox.x + font.h + margin, y = bbox.y, w = text_w + margin, h = bbox.h } 1384 | table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = check_rect, color = col } ) 1385 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = text, bbox = text_rect, color = colors.text } ) 1386 | 1387 | if checked and type( checked ) == "boolean" then 1388 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = "✔", bbox = check_rect, color = colors.check_mark } ) 1389 | end 1390 | 1391 | return result 1392 | end 1393 | 1394 | function UI2D.ToggleButton( text, checked, tooltip ) 1395 | local cur_window = windows[ begin_idx ] 1396 | local text_w = font.handle:getWidth( text ) 1397 | 1398 | local bbox = {} 1399 | if layout.same_line then 1400 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = (2 * font.h) + margin + text_w, h = (2 * margin) + font.h } 1401 | elseif layout.same_column then 1402 | bbox = { x = layout.x, y = layout.y + layout.h + margin, w = (2 * font.h) + margin + text_w, h = (2 * margin) + font.h } 1403 | else 1404 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = (2 * font.h) + margin + text_w, h = (2 * margin) + font.h } 1405 | end 1406 | 1407 | UpdateLayout( bbox ) 1408 | 1409 | local result = false 1410 | local col_border = colors.toggle_border 1411 | 1412 | if not modal_window or (modal_window and modal_window == cur_window) then 1413 | if PointInRect( mouse.x, mouse.y, bbox.x + cur_window.x, bbox.y + cur_window.y, bbox.w, bbox.h ) and cur_window == active_window then 1414 | if tooltip then 1415 | active_tooltip.text = tooltip 1416 | active_tooltip.x = mouse.x 1417 | active_tooltip.y = mouse.y 1418 | end 1419 | col_border = colors.toggle_border_hover 1420 | if mouse.state == e_mouse_state.clicked then 1421 | active_widget = cur_window.id .. text 1422 | end 1423 | if mouse.state == e_mouse_state.released and active_widget == cur_window.id .. text then 1424 | active_widget = nil 1425 | result = true 1426 | end 1427 | end 1428 | end 1429 | 1430 | local half_left = { x = bbox.x, y = bbox.y + margin, w = font.h, h = font.h } 1431 | local half_right = { x = bbox.x + font.h, y = bbox.y + margin, w = font.h, h = font.h } 1432 | local middle = { x = bbox.x + (font.h / 2), y = bbox.y + margin, w = font.h, h = font.h } 1433 | local text_rect = { x = bbox.x + (2 * font.h) + margin, y = bbox.y, w = text_w + margin, h = bbox.h } 1434 | 1435 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = text, bbox = text_rect, color = colors.text } ) 1436 | 1437 | if checked and type( checked ) == "boolean" then 1438 | table.insert( windows[ begin_idx ].command_list, { type = "circle_fill_half", bbox = half_left, color = colors.toggle_bg_on, angle1 = math.pi / 2, angle2 = math.pi * 1.5 } ) 1439 | table.insert( windows[ begin_idx ].command_list, { type = "circle_fill_half", bbox = half_right, color = colors.toggle_bg_on, angle1 = -math.pi / 2, angle2 = math.pi / 2 } ) 1440 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = middle, color = colors.toggle_bg_on } ) 1441 | table.insert( windows[ begin_idx ].command_list, { type = "circle_fill", bbox = half_right, color = colors.toggle_handle } ) 1442 | else 1443 | table.insert( windows[ begin_idx ].command_list, { type = "circle_fill_half", bbox = half_left, color = colors.toggle_bg_off, angle1 = math.pi / 2, angle2 = math.pi * 1.5 } ) 1444 | table.insert( windows[ begin_idx ].command_list, { type = "circle_fill_half", bbox = half_right, color = colors.toggle_bg_off, angle1 = -math.pi / 2, angle2 = math.pi / 2 } ) 1445 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = middle, color = colors.toggle_bg_off } ) 1446 | table.insert( windows[ begin_idx ].command_list, { type = "circle_fill", bbox = half_left, color = colors.toggle_handle } ) 1447 | end 1448 | 1449 | table.insert( windows[ begin_idx ].command_list, { type = "circle_wire_half", bbox = half_left, color = col_border, angle1 = math.pi / 2, angle2 = math.pi * 1.5 } ) 1450 | table.insert( windows[ begin_idx ].command_list, { type = "circle_wire_half", bbox = half_right, color = col_border, angle1 = -math.pi / 2, angle2 = math.pi / 2 } ) 1451 | table.insert( windows[ begin_idx ].command_list, 1452 | { type = "line", x1 = bbox.x + (font.h / 2), y1 = bbox.y + margin, x2 = bbox.x + (font.h * 1.5), y2 = bbox.y + margin, color = col_border } ) 1453 | table.insert( windows[ begin_idx ].command_list, 1454 | { type = "line", x1 = bbox.x + (font.h / 2), y1 = bbox.y + margin + font.h, x2 = bbox.x + (font.h * 1.5), y2 = bbox.y + margin + font.h, color = col_border } ) 1455 | return result 1456 | end 1457 | 1458 | function UI2D.RadioButton( text, checked, tooltip ) 1459 | local cur_window = windows[ begin_idx ] 1460 | local text_w = font.handle:getWidth( text ) 1461 | 1462 | local bbox = {} 1463 | if layout.same_line then 1464 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = font.h + margin + text_w, h = (2 * margin) + font.h } 1465 | elseif layout.same_column then 1466 | bbox = { x = layout.x, y = layout.y + layout.h + margin, w = font.h + margin + text_w, h = (2 * margin) + font.h } 1467 | else 1468 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = font.h + margin + text_w, h = (2 * margin) + font.h } 1469 | end 1470 | 1471 | UpdateLayout( bbox ) 1472 | 1473 | local result = false 1474 | local col = colors.radio_border 1475 | 1476 | if not modal_window or (modal_window and modal_window == cur_window) then 1477 | if PointInRect( mouse.x, mouse.y, bbox.x + cur_window.x, bbox.y + cur_window.y, bbox.w, bbox.h ) and cur_window == active_window then 1478 | if tooltip then 1479 | active_tooltip.text = tooltip 1480 | active_tooltip.x = mouse.x 1481 | active_tooltip.y = mouse.y 1482 | end 1483 | col = colors.radio_border_hover 1484 | 1485 | if mouse.state == e_mouse_state.clicked then 1486 | active_widget = cur_window.id .. text 1487 | end 1488 | if mouse.state == e_mouse_state.released and active_widget == cur_window.id .. text then 1489 | active_widget = nil 1490 | result = true 1491 | end 1492 | end 1493 | end 1494 | 1495 | local check_rect = { x = bbox.x, y = bbox.y + margin, w = font.h, h = font.h } 1496 | local text_rect = { x = bbox.x + font.h + margin, y = bbox.y, w = text_w + margin, h = bbox.h } 1497 | table.insert( windows[ begin_idx ].command_list, { type = "circle_wire", bbox = check_rect, color = col } ) 1498 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = text, bbox = text_rect, color = colors.text } ) 1499 | 1500 | if checked and type( checked ) == "boolean" then 1501 | table.insert( windows[ begin_idx ].command_list, { type = "circle_fill", bbox = check_rect, color = colors.radio_mark } ) 1502 | end 1503 | 1504 | return result 1505 | end 1506 | 1507 | function UI2D.TextBox( name, num_visible_chars, text, tooltip ) 1508 | local cur_window = windows[ begin_idx ] 1509 | local label = GetLabelPart( name ) 1510 | local label_w = font.handle:getWidth( label ) 1511 | 1512 | local bbox = {} 1513 | if layout.same_line then 1514 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = (4 * margin) + (num_visible_chars * font.w) + label_w, h = (2 * margin) + font.h } 1515 | elseif layout.same_column then 1516 | bbox = { x = layout.x, y = layout.y + layout.h + margin, w = (4 * margin) + (num_visible_chars * font.w) + label_w, h = (2 * margin) + font.h } 1517 | else 1518 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = (4 * margin) + (num_visible_chars * font.w) + label_w, h = (2 * margin) + font.h } 1519 | end 1520 | 1521 | UpdateLayout( bbox ) 1522 | 1523 | local scroll = 0 1524 | if active_textbox and active_textbox.id == cur_window.id .. name then 1525 | scroll = active_textbox.scroll 1526 | end 1527 | 1528 | local text_rect = { x = bbox.x, y = bbox.y, w = (2 * margin) + (num_visible_chars * font.w), h = bbox.h } 1529 | local visible_text = nil 1530 | if utf8.len( text ) > num_visible_chars then 1531 | visible_text = utf8.sub( text, scroll + 1, scroll + num_visible_chars ) 1532 | else 1533 | visible_text = text 1534 | end 1535 | local label_rect = { x = text_rect.x + text_rect.w + margin, y = bbox.y, w = label_w, h = bbox.h } 1536 | local char_rect = { x = text_rect.x + margin, y = text_rect.y, w = (utf8.len( visible_text ) * font.w), h = text_rect.h } 1537 | 1538 | -- Text editing 1539 | local caret_rect = nil 1540 | if active_widget == cur_window.id .. name then 1541 | if text_input_character then 1542 | local p = active_textbox.caret + active_textbox.scroll 1543 | local part1 = utf8.sub( text, 1, p ) 1544 | local part2 = utf8.sub( text, p + 1, utf8.len( text ) ) 1545 | text = part1 .. text_input_character .. part2 1546 | active_textbox.caret = active_textbox.caret + 1 1547 | if active_textbox.caret > num_visible_chars then 1548 | active_textbox.scroll = active_textbox.scroll + 1 1549 | end 1550 | end 1551 | 1552 | if keys[ "backspace" ][ 3 ] == 1 or repeating_key == "backspace" then 1553 | if active_textbox.caret > 0 then 1554 | local p = active_textbox.caret + active_textbox.scroll 1555 | local part1 = utf8.sub( text, 1, p - 1 ) 1556 | local part2 = utf8.sub( text, p + 1, utf8.len( text ) ) 1557 | text = part1 .. part2 1558 | 1559 | local max_scroll = utf8.len( text ) - num_visible_chars 1560 | if active_textbox.scroll < max_scroll or utf8.len( text ) < num_visible_chars then 1561 | active_textbox.caret = active_textbox.caret - 1 1562 | end 1563 | end 1564 | end 1565 | 1566 | if keys[ "delete" ][ 3 ] == 1 or repeating_key == "delete" then 1567 | if active_textbox.caret < num_visible_chars and active_textbox.caret < utf8.len( text ) then 1568 | local p = active_textbox.caret + active_textbox.scroll 1569 | local part1 = utf8.sub( text, 1, p ) 1570 | local part2 = utf8.sub( text, p + 2, utf8.len( text ) ) 1571 | text = part1 .. part2 1572 | 1573 | local max_scroll = utf8.len( text ) - num_visible_chars 1574 | if active_textbox.scroll >= max_scroll and utf8.len( text ) > num_visible_chars then 1575 | active_textbox.caret = active_textbox.caret + 1 1576 | end 1577 | end 1578 | end 1579 | 1580 | if keys[ "left" ][ 3 ] == 1 or repeating_key == "left" then 1581 | if active_textbox.caret == 0 then 1582 | if active_textbox.scroll > 0 then 1583 | active_textbox.scroll = active_textbox.scroll - 1 1584 | end 1585 | end 1586 | active_textbox.caret = active_textbox.caret - 1 1587 | end 1588 | 1589 | if keys[ "right" ][ 3 ] == 1 or repeating_key == "right" then 1590 | local full_length = utf8.len( text ) 1591 | local visible_length = utf8.len( visible_text ) 1592 | if active_textbox.caret == num_visible_chars and full_length > num_visible_chars and active_textbox.scroll < (full_length - visible_length) then 1593 | active_textbox.scroll = active_textbox.scroll + 1 1594 | end 1595 | if active_textbox.caret < full_length then 1596 | active_textbox.caret = active_textbox.caret + 1 1597 | end 1598 | end 1599 | 1600 | local max_scroll = utf8.len( text ) - num_visible_chars 1601 | if max_scroll < 0 then max_scroll = 0 end 1602 | active_textbox.scroll = Clamp( active_textbox.scroll, 0, max_scroll ) 1603 | scroll = active_textbox.scroll 1604 | active_textbox.caret = Clamp( active_textbox.caret, 0, num_visible_chars ) 1605 | caret_rect = { x = char_rect.x + (active_textbox.caret * font.w), y = char_rect.y + margin, w = 2, h = font.h } 1606 | end 1607 | 1608 | local col1 = colors.textbox_bg 1609 | local col2 = colors.textbox_border 1610 | 1611 | if not modal_window or (modal_window and modal_window == cur_window) then 1612 | if PointInRect( mouse.x, mouse.y, text_rect.x + cur_window.x, text_rect.y + cur_window.y, text_rect.w, text_rect.h ) and cur_window == active_window then 1613 | if tooltip then 1614 | active_tooltip.text = tooltip 1615 | active_tooltip.x = mouse.x 1616 | active_tooltip.y = mouse.y 1617 | end 1618 | col1 = colors.textbox_bg_hover 1619 | 1620 | if mouse.state == e_mouse_state.clicked then 1621 | has_text_input = true 1622 | local pos = math.floor( (mouse.x - cur_window.x - text_rect.x) / font.w ) 1623 | if pos > utf8.len( text ) then 1624 | pos = utf8.len( text ) 1625 | end 1626 | 1627 | if active_widget ~= cur_window.id .. name then 1628 | active_textbox = { id = cur_window.id .. name, caret = pos } 1629 | active_textbox.scroll = 0 1630 | active_widget = cur_window.id .. name 1631 | else 1632 | active_textbox.caret = pos 1633 | end 1634 | end 1635 | else 1636 | if mouse.state == e_mouse_state.clicked then 1637 | if active_widget == cur_window.id .. name then -- Deactivate self 1638 | has_text_input = false 1639 | active_textbox = nil 1640 | active_widget = nil 1641 | return text, true 1642 | end 1643 | end 1644 | end 1645 | 1646 | if active_widget == cur_window.id .. name then 1647 | if keys[ "tab" ][ 3 ] == 1 or keys[ "return" ][ 3 ] == 1 or keys[ "kpenter" ][ 3 ] == 1 then -- Deactivate self 1648 | has_text_input = false 1649 | active_textbox = nil 1650 | active_widget = nil 1651 | return text, true 1652 | end 1653 | end 1654 | end 1655 | 1656 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = text_rect, color = col1 } ) 1657 | table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = text_rect, color = col2 } ) 1658 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = visible_text, bbox = char_rect, color = colors.text } ) 1659 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = label, bbox = label_rect, color = colors.text } ) 1660 | 1661 | if caret_rect and caret_blink.on then 1662 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = caret_rect, color = colors.text } ) 1663 | end 1664 | 1665 | return text, false 1666 | end 1667 | 1668 | function UI2D.ListBoxSetSelected( name, idx ) 1669 | local exists, lst_idx = ListBoxExists( name ) 1670 | if exists then 1671 | if type( idx ) == "table" then 1672 | listbox_state[ lst_idx ].selection = {} 1673 | for i, v in ipairs( idx ) do 1674 | table.insert( listbox_state[ lst_idx ].selection, v ) 1675 | end 1676 | else 1677 | listbox_state[ lst_idx ].selected_idx = idx 1678 | end 1679 | end 1680 | end 1681 | 1682 | function UI2D.ListBox( name, num_visible_rows, num_visible_chars, collection, selected, multi_select, tooltip ) 1683 | local cur_window = windows[ begin_idx ] 1684 | local exists, lst_idx = ListBoxExists( cur_window.id .. name ) 1685 | 1686 | if not exists then 1687 | local selected_idx = 0 1688 | if type( selected ) == "number" then 1689 | selected_idx = selected 1690 | elseif type( selected ) == "string" then 1691 | for i = 1, #collection do 1692 | if selected == collection[ i ] then 1693 | selected_idx = i 1694 | break 1695 | end 1696 | end 1697 | end 1698 | local lb = { id = cur_window.id .. name, selected_idx = selected_idx, scroll_x = 0, scroll_y = 0, selection = {} } 1699 | if selected_idx > 0 then 1700 | table.insert( lb.selection, selected_idx ) 1701 | end 1702 | table.insert( listbox_state, lb ) 1703 | end 1704 | 1705 | if lst_idx == 0 then 1706 | lst_idx = #listbox_state 1707 | end 1708 | 1709 | local sbt = font.h -- scrollbar thickness 1710 | local bbox = {} 1711 | if layout.same_line then 1712 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = (2 * margin) + (num_visible_chars * font.w) + sbt, h = (num_visible_rows * font.h) + sbt } 1713 | elseif layout.same_column then 1714 | bbox = { x = layout.x, y = layout.y + layout.h + margin, w = (2 * margin) + (num_visible_chars * font.w) + sbt, h = (num_visible_rows * font.h) + sbt } 1715 | else 1716 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = (2 * margin) + (num_visible_chars * font.w) + sbt, h = (num_visible_rows * font.h) + sbt } 1717 | end 1718 | 1719 | UpdateLayout( bbox ) 1720 | 1721 | local sb_vertical = { x = bbox.x + bbox.w - sbt, y = bbox.y + sbt, w = sbt, h = bbox.h - (3 * sbt) } 1722 | local sb_horizontal = { x = bbox.x + sbt, y = bbox.y + bbox.h - sbt, w = bbox.w - (3 * sbt), h = sbt } 1723 | local sb_button_top = { x = bbox.x + bbox.w - sbt, y = bbox.y, w = sbt, h = sbt } 1724 | local sb_button_bottom = { x = bbox.x + bbox.w - sbt, y = bbox.y + bbox.h - (2 * sbt), w = sbt, h = sbt } 1725 | local sb_button_left = { x = bbox.x, y = bbox.y + bbox.h - sbt, w = sbt, h = sbt } 1726 | local sb_button_right = { x = bbox.x + bbox.w - (2 * sbt), y = bbox.y + bbox.h - sbt, w = sbt, h = sbt } 1727 | 1728 | local max_total_chars_x = GetLongerStringLen( collection ) 1729 | local highlight_idx = nil 1730 | local result = false 1731 | 1732 | -- Input for buttons and selection 1733 | local t_btn_col = colors.list_button 1734 | local b_btn_col = colors.list_button 1735 | local l_btn_col = colors.list_button 1736 | local r_btn_col = colors.list_button 1737 | if not modal_window or (modal_window and modal_window == cur_window) then 1738 | if cur_window == active_window then 1739 | if PointInRect( mouse.x, mouse.y, bbox.x + cur_window.x, bbox.y + cur_window.y, bbox.w, bbox.h ) then -- whole listbox 1740 | if tooltip then 1741 | active_tooltip.text = tooltip 1742 | active_tooltip.x = mouse.x 1743 | active_tooltip.y = mouse.y 1744 | end 1745 | listbox_state[ lst_idx ].scroll_y = listbox_state[ lst_idx ].scroll_y - mouse.wheel_y 1746 | listbox_state[ lst_idx ].scroll_x = listbox_state[ lst_idx ].scroll_x - mouse.wheel_x 1747 | end 1748 | 1749 | if PointInRect( mouse.x, mouse.y, bbox.x + cur_window.x, bbox.y + cur_window.y, bbox.w - sbt, bbox.h - sbt ) and #collection > 0 then -- content area 1750 | highlight_idx = math.floor( (mouse.y - cur_window.y - bbox.y) / (font.h) ) + 1 1751 | highlight_idx = Clamp( highlight_idx, 1, #collection ) 1752 | 1753 | if mouse.state == e_mouse_state.clicked then 1754 | listbox_state[ lst_idx ].selected_idx = highlight_idx + listbox_state[ lst_idx ].scroll_y 1755 | result = true 1756 | if multi_select then 1757 | if framework.GetKeyDown( "lctrl" ) then 1758 | local exists = false 1759 | local idx = 0 1760 | for i, v in ipairs( listbox_state[ lst_idx ].selection ) do 1761 | if v == listbox_state[ lst_idx ].selected_idx then 1762 | idx = i 1763 | exists = true 1764 | break 1765 | end 1766 | end 1767 | if not exists then 1768 | table.insert( listbox_state[ lst_idx ].selection, listbox_state[ lst_idx ].selected_idx ) 1769 | else 1770 | table.remove( listbox_state[ lst_idx ].selection, idx ) 1771 | end 1772 | else 1773 | listbox_state[ lst_idx ].selection = {} 1774 | table.insert( listbox_state[ lst_idx ].selection, listbox_state[ lst_idx ].selected_idx ) 1775 | end 1776 | end 1777 | end 1778 | elseif PointInRect( mouse.x, mouse.y, sb_vertical.x + cur_window.x, sb_vertical.y + cur_window.y, sbt, sb_vertical.h ) then -- v_scrollbar 1779 | elseif PointInRect( mouse.x, mouse.y, sb_horizontal.x + cur_window.x, sb_horizontal.y + cur_window.y, sb_horizontal.w, sbt ) then -- h_scrollbar 1780 | elseif PointInRect( mouse.x, mouse.y, sb_button_top.x + cur_window.x, sb_button_top.y + cur_window.y, sb_button_top.w, sbt ) then -- button top 1781 | t_btn_col = colors.list_button_hover 1782 | if mouse.state == e_mouse_state.clicked then 1783 | listbox_state[ lst_idx ].scroll_y = listbox_state[ lst_idx ].scroll_y - 1 1784 | t_btn_col = colors.list_button_click 1785 | end 1786 | elseif PointInRect( mouse.x, mouse.y, sb_button_bottom.x + cur_window.x, sb_button_bottom.y + cur_window.y, sb_button_bottom.w, sbt ) then -- button bottom 1787 | b_btn_col = colors.list_button_hover 1788 | if mouse.state == e_mouse_state.clicked then 1789 | listbox_state[ lst_idx ].scroll_y = listbox_state[ lst_idx ].scroll_y + 1 1790 | b_btn_col = colors.list_button_click 1791 | end 1792 | elseif PointInRect( mouse.x, mouse.y, sb_button_left.x + cur_window.x, sb_button_left.y + cur_window.y, sb_button_left.w, sbt ) then -- button left 1793 | l_btn_col = colors.list_button_hover 1794 | if mouse.state == e_mouse_state.clicked then 1795 | listbox_state[ lst_idx ].scroll_x = listbox_state[ lst_idx ].scroll_x - 1 1796 | l_btn_col = colors.list_button_click 1797 | end 1798 | elseif PointInRect( mouse.x, mouse.y, sb_button_right.x + cur_window.x, sb_button_right.y + cur_window.y, sb_button_right.w, sbt ) then -- button right 1799 | r_btn_col = colors.list_button_hover 1800 | if mouse.state == e_mouse_state.clicked then 1801 | listbox_state[ lst_idx ].scroll_x = listbox_state[ lst_idx ].scroll_x + 1 1802 | r_btn_col = colors.list_button_click 1803 | end 1804 | end 1805 | end 1806 | end 1807 | 1808 | -- Draw scrollbars 1809 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = bbox, color = colors.list_bg } ) 1810 | table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = bbox, color = colors.list_border } ) 1811 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = sb_vertical, color = colors.list_track } ) 1812 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = sb_horizontal, color = colors.list_track } ) 1813 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = "△", bbox = sb_button_top, color = t_btn_col } ) 1814 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = "▽", bbox = sb_button_bottom, color = b_btn_col } ) 1815 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = "◁", bbox = sb_button_left, color = l_btn_col } ) 1816 | table.insert( windows[ begin_idx ].command_list, { type = "text", text = "▷", bbox = sb_button_right, color = r_btn_col } ) 1817 | 1818 | local max_scroll_y = 0 1819 | if #collection > num_visible_rows then 1820 | max_scroll_y = #collection - num_visible_rows 1821 | end 1822 | local max_scroll_x = max_total_chars_x - num_visible_chars - 1 1823 | if max_scroll_x < 0 then 1824 | max_scroll_x = 0 1825 | end 1826 | 1827 | listbox_state[ lst_idx ].scroll_y = Clamp( listbox_state[ lst_idx ].scroll_y, 0, max_scroll_y ) 1828 | listbox_state[ lst_idx ].scroll_x = Clamp( listbox_state[ lst_idx ].scroll_x, 0, max_scroll_x ) 1829 | 1830 | local scroll_y = listbox_state[ lst_idx ].scroll_y 1831 | local scroll_x = listbox_state[ lst_idx ].scroll_x 1832 | local first = scroll_y + 1 1833 | local last = scroll_y + num_visible_rows 1834 | if #collection < num_visible_rows then 1835 | last = #collection 1836 | end 1837 | 1838 | -- Input for thumbs 1839 | if not modal_window or (modal_window and modal_window == cur_window) then 1840 | -- thumb vertical 1841 | if max_scroll_y > 0 then 1842 | local v_thumb_height = sb_vertical.h * (num_visible_rows / #collection) 1843 | local max_dist = sb_vertical.h - v_thumb_height 1844 | local scroll_distance = MapRange( 0, max_scroll_y, 0, max_dist, scroll_y ) 1845 | local thumb_vertical = { x = bbox.x + bbox.w - sbt, y = bbox.y + sbt + scroll_distance, w = sbt, h = v_thumb_height } 1846 | 1847 | local col = colors.list_thumb 1848 | if PointInRect( mouse.x, mouse.y, thumb_vertical.x + cur_window.x, thumb_vertical.y + cur_window.y, sbt, thumb_vertical.h ) then 1849 | col = colors.list_thumb_hover 1850 | if mouse.state == e_mouse_state.clicked then 1851 | listbox_state[ lst_idx ].mouse_start_y = mouse.y 1852 | listbox_state[ lst_idx ].old_scroll_y = listbox_state[ lst_idx ].scroll_y 1853 | end 1854 | end 1855 | 1856 | if mouse.state == e_mouse_state.held and listbox_state[ lst_idx ].mouse_start_y then 1857 | col = colors.list_thumb_click 1858 | local pixel_steps = max_scroll_y / font.h 1859 | local diff = mouse.y - listbox_state[ lst_idx ].mouse_start_y 1860 | listbox_state[ lst_idx ].scroll_y = math.floor( diff / pixel_steps ) + listbox_state[ lst_idx ].old_scroll_y 1861 | end 1862 | 1863 | if mouse.state == e_mouse_state.released and listbox_state[ lst_idx ].mouse_start_y then 1864 | listbox_state[ lst_idx ].mouse_start_y = nil 1865 | listbox_state[ lst_idx ].old_scroll_y = nil 1866 | end 1867 | 1868 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = thumb_vertical, color = col } ) 1869 | end 1870 | 1871 | -- thumb horizontal 1872 | if max_scroll_x > 0 then 1873 | local h_thumb_width = sb_horizontal.w * (num_visible_chars / max_total_chars_x) 1874 | local max_dist = sb_horizontal.w - h_thumb_width 1875 | local scroll_distance = MapRange( 0, max_scroll_x, 0, max_dist, scroll_x ) 1876 | local thumb_horizontal = { x = bbox.x + sbt + scroll_distance, y = bbox.y + bbox.h - sbt, w = h_thumb_width, h = sbt } 1877 | 1878 | local col = colors.list_thumb 1879 | if PointInRect( mouse.x, mouse.y, thumb_horizontal.x + cur_window.x, thumb_horizontal.y + cur_window.y, thumb_horizontal.w, sbt ) then 1880 | col = colors.list_thumb_hover 1881 | if mouse.state == e_mouse_state.clicked then 1882 | listbox_state[ lst_idx ].mouse_start_x = mouse.x 1883 | listbox_state[ lst_idx ].old_scroll_x = listbox_state[ lst_idx ].scroll_x 1884 | end 1885 | end 1886 | 1887 | if mouse.state == e_mouse_state.held and listbox_state[ lst_idx ].mouse_start_x then 1888 | col = colors.list_thumb_click 1889 | local pixel_steps = max_scroll_x / font.h 1890 | local diff = mouse.x - listbox_state[ lst_idx ].mouse_start_x 1891 | listbox_state[ lst_idx ].scroll_x = math.floor( diff / pixel_steps ) + listbox_state[ lst_idx ].old_scroll_x 1892 | end 1893 | 1894 | if mouse.state == e_mouse_state.released and listbox_state[ lst_idx ].mouse_start_x then 1895 | listbox_state[ lst_idx ].mouse_start_x = nil 1896 | listbox_state[ lst_idx ].old_scroll_x = nil 1897 | end 1898 | 1899 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = thumb_horizontal, color = col } ) 1900 | end 1901 | end 1902 | 1903 | -- Draw selected rect 1904 | if multi_select then 1905 | for i, v in ipairs( listbox_state[ lst_idx ].selection ) do 1906 | local sel_idx = v 1907 | if sel_idx >= first and sel_idx <= last then 1908 | local selected_rect = { x = bbox.x, y = bbox.y + (sel_idx - scroll_y - 1) * font.h, w = bbox.w - sbt, h = font.h } 1909 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = selected_rect, color = colors.list_selected } ) 1910 | end 1911 | end 1912 | else 1913 | local sel_idx = listbox_state[ lst_idx ].selected_idx 1914 | if sel_idx >= first and sel_idx <= last then 1915 | local selected_rect = { x = bbox.x, y = bbox.y + (sel_idx - scroll_y - 1) * font.h, w = bbox.w - sbt, h = font.h } 1916 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = selected_rect, color = colors.list_selected } ) 1917 | end 1918 | end 1919 | 1920 | -- Draw highlight rect 1921 | if highlight_idx then 1922 | local highlight_rect = { x = bbox.x, y = bbox.y + ((highlight_idx - 1) * font.h), w = bbox.w - sbt, h = font.h } 1923 | table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = highlight_rect, color = colors.list_highlight } ) 1924 | end 1925 | 1926 | -- Draw entries 1927 | local y_offset = bbox.y 1928 | for i = first, last do 1929 | local final_str = nil 1930 | local cur = collection[ i ] 1931 | local cur_len = utf8.len( cur ) 1932 | 1933 | if cur_len - scroll_x > num_visible_chars + 1 then 1934 | final_str = utf8.sub( cur, scroll_x + 1, num_visible_chars + scroll_x + 1 ) 1935 | else 1936 | if scroll_x < cur_len then 1937 | final_str = utf8.sub( cur, scroll_x + 1, cur_len ) 1938 | else 1939 | final_str = nil 1940 | end 1941 | end 1942 | 1943 | if final_str then 1944 | local final_len = utf8.len( final_str ) 1945 | local item_w = final_len * font.w 1946 | table.insert( windows[ begin_idx ].command_list, 1947 | { type = "text", text = final_str, bbox = { x = bbox.x, y = y_offset, w = item_w + margin, h = font.h }, color = colors.text } ) 1948 | end 1949 | y_offset = y_offset + font.h 1950 | end 1951 | 1952 | if #collection > 0 then 1953 | listbox_state[ lst_idx ].selected_idx = Clamp( listbox_state[ lst_idx ].selected_idx, 0, #collection ) 1954 | end 1955 | local t = {} 1956 | if multi_select then 1957 | t = listbox_state[ lst_idx ].selection 1958 | end 1959 | return result, listbox_state[ lst_idx ].selected_idx, t 1960 | end 1961 | 1962 | function UI2D.CustomWidget( name, width, height, tooltip ) 1963 | local cur_window = windows[ begin_idx ] 1964 | local exists, idx = WidgetExists( cur_window, cur_window.id .. name ) 1965 | 1966 | if not exists then 1967 | local new_widget = {} 1968 | new_widget.id = cur_window.id .. name 1969 | new_widget.width = width 1970 | new_widget.height = height 1971 | new_widget.texture = framework.NewTexture( width, height ) 1972 | new_widget.pass = framework.NewPass( new_widget.texture ) 1973 | framework.SetProjection( new_widget.pass ) 1974 | table.insert( cur_window.cw, new_widget ) 1975 | idx = #cur_window.cw 1976 | else 1977 | if cur_window.cw[ idx ].width ~= width or cur_window.cw[ idx ].height ~= height then 1978 | cur_window.cw[ idx ].width = width 1979 | cur_window.cw[ idx ].height = height 1980 | framework.ReleaseTexture() 1981 | cur_window.cw[ idx ].texture = framework.NewTexture( width, height ) 1982 | framework.SetCanvas( cur_window.cw[ idx ].pass, cur_window.cw[ idx ].texture ) 1983 | end 1984 | end 1985 | 1986 | local bbox = {} 1987 | if layout.same_line then 1988 | bbox = { x = layout.x + layout.w + margin, y = layout.y, w = width, h = height } 1989 | elseif layout.same_column then 1990 | bbox = { x = layout.x, y = layout.y + layout.h + margin, w = width, h = height } 1991 | else 1992 | bbox = { x = margin, y = layout.y + layout.row_h + margin, w = width, h = height } 1993 | end 1994 | 1995 | UpdateLayout( bbox ) 1996 | 1997 | local clicked = false 1998 | local held = false 1999 | local released = false 2000 | local hovered = false 2001 | local wheelx, wheely = 0, 0 2002 | 2003 | if not modal_window or (modal_window and modal_window == cur_window) then 2004 | if PointInRect( mouse.x, mouse.y, bbox.x + cur_window.x, bbox.y + cur_window.y, bbox.w, bbox.h ) and cur_window == active_window then 2005 | if tooltip then 2006 | active_tooltip.text = tooltip 2007 | active_tooltip.x = mouse.x 2008 | active_tooltip.y = mouse.y 2009 | end 2010 | hovered = true 2011 | wheelx, wheely = mouse.wheel_x, mouse.wheel_y 2012 | 2013 | if mouse.state == e_mouse_state.clicked then 2014 | clicked = true 2015 | active_widget = cur_window.cw[ idx ] 2016 | end 2017 | end 2018 | 2019 | if mouse.state == e_mouse_state.held and cur_window == active_window and active_widget == cur_window.cw[ idx ] then 2020 | held = true 2021 | end 2022 | if mouse.state == e_mouse_state.released and cur_window == active_window and active_widget == cur_window.cw[ idx ] then 2023 | released = true 2024 | active_widget = nil 2025 | end 2026 | end 2027 | 2028 | cur_window.cw[ idx ].bbox = bbox 2029 | table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = bbox, color = colors.button_border } ) 2030 | framework.ResetPass( cur_window.cw[ idx ].pass ) 2031 | framework.SetProjection( cur_window.cw[ idx ].pass ) 2032 | if framework.type == "lovr" then 2033 | return cur_window.cw[ idx ].pass, clicked, held, released, hovered, mouse.x - cur_window.x - bbox.x, mouse.y - cur_window.y - bbox.y, wheelx, wheely 2034 | else 2035 | return cur_window.cw[ idx ].texture, clicked, held, released, hovered, mouse.x - cur_window.x - bbox.x, mouse.y - cur_window.y - bbox.y, wheelx, wheely 2036 | end 2037 | end 2038 | 2039 | function UI2D.RenderFrame( main_pass ) 2040 | assert( begin_end_pairs.b == begin_end_pairs.e, "Begin/End pairs don't match! Begin calls: " .. begin_end_pairs.b .. " - End calls: " .. begin_end_pairs.e ) 2041 | begin_end_pairs.b = 0 2042 | begin_end_pairs.e = 0 2043 | table.sort( windows, function( a, b ) return a.z > b.z end ) 2044 | framework.SetCanvas() 2045 | 2046 | local count = #windows 2047 | for i = count, 1, -1 do 2048 | local win = windows[ i ] 2049 | 2050 | if win.was_called_this_frame then 2051 | framework.SetColor( main_pass, { 1, 1, 1 } ) 2052 | if modal_window and win ~= modal_window then 2053 | framework.SetColor( main_pass, colors.modal_tint ) 2054 | end 2055 | if framework.type == "lovr" then 2056 | framework.SetMaterial( main_pass, win.texture ) 2057 | framework.DrawRect( main_pass, win.x + (win.w / 2), win.y + (win.h / 2), win.w, -win.h, "fill" ) --NOTE flip Y fix 2058 | framework.SetMaterial( main_pass ) 2059 | else 2060 | love.graphics.draw( win.texture, win.x, win.y ) 2061 | end 2062 | for j, k in ipairs( windows[ i ].cw ) do 2063 | framework.SetColor( win.pass, { 1, 1, 1 } ) 2064 | framework.SetMaterial( win.pass, k.texture ) 2065 | if framework.type == "lovr" then 2066 | framework.DrawRect( win.pass, k.bbox.x + (k.bbox.w / 2), k.bbox.y + (k.bbox.h / 2), k.bbox.w, -k.bbox.h, "fill" ) 2067 | else 2068 | love.graphics.draw( k.texture, windows[ i ].x + k.bbox.x, windows[ i ].y + k.bbox.y ) 2069 | end 2070 | framework.SetMaterial( win.pass ) 2071 | framework.SetColor( win.pass, { 1, 1, 1 } ) 2072 | end 2073 | if i == 1 and active_tooltip.text ~= "" then -- Draw tooltip 2074 | local num_lines = GetLineCount( active_tooltip.text ) 2075 | local text_w = font.handle:getWidth( active_tooltip.text ) 2076 | local rect_x = active_tooltip.x + (text_w / 2) + font.h 2077 | local rect_w = text_w + (2 * margin) 2078 | local rect_h = (num_lines * font.h) + (2 * margin) 2079 | local rect_y = active_tooltip.y - (rect_h / 2) 2080 | local text_y = 0 2081 | 2082 | local text_x = active_tooltip.x + font.h 2083 | if framework.type == "lovr" then 2084 | text_y = rect_y - margin 2085 | else 2086 | text_y = active_tooltip.y - (font.h / 2) - (num_lines * font.h) 2087 | end 2088 | 2089 | local width, height 2090 | if framework.type == "lovr" then 2091 | width, height = lovr.system.getWindowDimensions() 2092 | else 2093 | width, height = love.window.getMode() 2094 | end 2095 | if mouse.x > width - rect_w - margin then 2096 | rect_x = active_tooltip.x - (text_w / 2) - font.h 2097 | text_x = active_tooltip.x - font.h - text_w 2098 | end 2099 | 2100 | if mouse.y < rect_h then 2101 | rect_y = active_tooltip.y + (rect_h / 2) 2102 | text_y = active_tooltip.y + (font.h / 2) 2103 | end 2104 | 2105 | framework.SetColor( main_pass, colors.tooltip_bg ) 2106 | framework.DrawRect( main_pass, rect_x, rect_y, rect_w, rect_h, "fill" ) 2107 | framework.SetColor( main_pass, colors.tooltip_border ) 2108 | framework.DrawRect( main_pass, rect_x, rect_y, rect_w, rect_h, "line" ) 2109 | 2110 | framework.SetColor( main_pass, colors.text ) 2111 | framework.SetFont( main_pass ) 2112 | framework.DrawText( main_pass, active_tooltip.text, text_x, text_y, text_w, font.h, text_w ) 2113 | active_tooltip.text = "" 2114 | end 2115 | else 2116 | table.remove( windows, i ) 2117 | end 2118 | end 2119 | 2120 | mouse.wheel_x = 0 2121 | mouse.wheel_y = 0 2122 | 2123 | text_input_character = nil 2124 | local passes = {} 2125 | 2126 | for i, v in ipairs( windows ) do 2127 | v.command_list = nil 2128 | v.command_list = {} 2129 | v.was_called_this_frame = false 2130 | 2131 | if framework.type == "lovr" then 2132 | for j, k in ipairs( v.cw ) do 2133 | table.insert( passes, k.pass ) 2134 | end 2135 | table.insert( passes, v.pass ) 2136 | end 2137 | end 2138 | 2139 | if framework.type == "lovr" then 2140 | return passes 2141 | end 2142 | end 2143 | 2144 | return UI2D 2145 | --------------------------------------------------------------------------------