├── .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 |
--------------------------------------------------------------------------------