ui2d.lua 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044
  1. local utf8 = require "utf8"
  2. local UI2D = {}
  3. local e_mouse_state = { clicked = 1, held = 2, released = 3, idle = 4 }
  4. local e_slider_type = { int = 1, float = 2 }
  5. local modal_window = nil
  6. local active_window = nil
  7. local active_widget = nil
  8. local active_textbox = nil
  9. local dragged_window = nil
  10. local repeating_key = nil
  11. local text_input_character = nil
  12. local begin_idx = nil
  13. local margin = 8
  14. local separator_thickness = 2
  15. local windows = {}
  16. local color_themes = {}
  17. local overriden_colors = {}
  18. local font = { handle = nil, w = nil, h = nil }
  19. local dragged_window_offset = { x = 0, y = 0 }
  20. local mouse = { x = 0, y = 0, state = e_mouse_state.idle, prev_frame = 0, this_frame = 0 }
  21. local texture_flags = { mipmaps = true, usage = { 'sample', 'render', 'transfer' } }
  22. 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 }
  23. local clamp_sampler = lovr.graphics.newSampler( { wrap = 'clamp' } )
  24. color_themes.dark =
  25. {
  26. text = { 0.8, 0.8, 0.8 },
  27. window_bg = { 0.26, 0.26, 0.26 },
  28. window_border = { 0, 0, 0 },
  29. window_titlebar = { 0.08, 0.08, 0.08 },
  30. window_titlebar_active = { 0, 0, 0 },
  31. button_bg = { 0.14, 0.14, 0.14 },
  32. button_bg_hover = { 0.19, 0.19, 0.19 },
  33. button_bg_click = { 0.12, 0.12, 0.12 },
  34. button_border = { 0, 0, 0 },
  35. check_border = { 0, 0, 0 },
  36. check_border_hover = { 0.5, 0.5, 0.5 },
  37. check_mark = { 0.3, 0.3, 1 },
  38. radio_border = { 0, 0, 0 },
  39. radio_border_hover = { 0.5, 0.5, 0.5 },
  40. radio_mark = { 0.3, 0.3, 1 },
  41. slider_bg = { 0.3, 0.3, 1 },
  42. slider_bg_hover = { 0.38, 0.38, 1 },
  43. slider_thumb = { 0.2, 0.2, 1 },
  44. list_bg = { 0.14, 0.14, 0.14 },
  45. list_border = { 0, 0, 0 },
  46. list_selected = { 0.3, 0.3, 1 },
  47. list_highlight = { 0.3, 0.3, 0.3 },
  48. textbox_bg = { 0.03, 0.03, 0.03 },
  49. textbox_bg_hover = { 0.11, 0.11, 0.11 },
  50. textbox_border = { 0.1, 0.1, 0.1 },
  51. textbox_border_focused = { 0.58, 0.58, 1 },
  52. image_button_border_highlight = { 0.5, 0.5, 0.5 },
  53. tab_bar_bg = { 0.1, 0.1, 0.1 },
  54. tab_bar_border = { 0, 0, 0 },
  55. tab_bar_hover = { 0.2, 0.2, 0.2 },
  56. tab_bar_highlight = { 0.3, 0.3, 1 },
  57. progress_bar_bg = { 0.2, 0.2, 0.2 },
  58. progress_bar_fill = { 0.3, 0.3, 1 },
  59. progress_bar_border = { 0, 0, 0 },
  60. osk_mode_bg = { 0, 0, 0 },
  61. osk_highlight = { 1, 1, 1 },
  62. modal_tint = { 0.3, 0.3, 0.3 },
  63. separator = { 0, 0, 0 }
  64. }
  65. color_themes.light =
  66. {
  67. check_border = { 0.000, 0.000, 0.000 },
  68. check_border_hover = { 0.760, 0.760, 0.760 },
  69. textbox_bg_hover = { 0.570, 0.570, 0.570 },
  70. textbox_border = { 0.000, 0.000, 0.000 },
  71. text = { 0.120, 0.120, 0.120 },
  72. button_bg_hover = { 0.900, 0.900, 0.900 },
  73. radio_mark = { 0.172, 0.172, 0.172 },
  74. slider_bg = { 0.830, 0.830, 0.830 },
  75. progress_bar_fill = { 0.830, 0.830, 1.000 },
  76. progress_bar_bg = { 1.000, 1.000, 1.000 },
  77. tab_bar_highlight = { 0.151, 0.140, 1.000 },
  78. tab_bar_hover = { 0.802, 0.797, 0.795 },
  79. tab_bar_border = { 0.000, 0.000, 0.000 },
  80. tab_bar_bg = { 1.000, 0.994, 0.999 },
  81. image_button_border_highlight = { 0.500, 0.500, 0.500 },
  82. textbox_bg = { 0.700, 0.700, 0.700 },
  83. window_border = { 0.000, 0.000, 0.000 },
  84. window_bg = { 0.930, 0.930, 0.930 },
  85. window_titlebar = { 0.8, 0.8, 0.8 },
  86. window_titlebar_active = { 0.9, 0.9, 0.9 },
  87. button_bg = { 0.800, 0.800, 0.800 },
  88. progress_bar_border = { 0.000, 0.000, 0.000 },
  89. slider_bg_hover = { 0.870, 0.870, 0.870 },
  90. slider_thumb = { 0.700, 0.700, 0.700 },
  91. list_bg = { 0.877, 0.883, 0.877 },
  92. list_border = { 0.000, 0.000, 0.000 },
  93. list_selected = { 0.686, 0.687, 0.688 },
  94. list_highlight = { 0.808, 0.810, 0.811 },
  95. check_mark = { 0.000, 0.000, 0.000 },
  96. radio_border = { 0.000, 0.000, 0.000 },
  97. radio_border_hover = { 0.760, 0.760, 0.760 },
  98. textbox_border_focused = { 0.000, 0.000, 1.000 },
  99. button_bg_click = { 0.120, 0.120, 0.120 },
  100. button_border = { 0.000, 0.000, 0.000 },
  101. osk_mode_bg = { 0.5, 0.5, 0.5 },
  102. osk_highlight = { 0.1, 0.1, 0.1 },
  103. modal_tint = { 0.15, 0.15, 0.15 },
  104. separator = { 0.5, 0.5, 0.5 }
  105. }
  106. local colors = color_themes.dark
  107. local function Clamp( n, n_min, n_max )
  108. if n < n_min then
  109. n = n_min
  110. elseif n > n_max then
  111. n = n_max
  112. end
  113. return n
  114. end
  115. local function GetLineCount( str )
  116. -- https://stackoverflow.com/questions/24690910/how-to-get-lines-count-in-string/70137660#70137660
  117. local lines = 1
  118. for i = 1, #str do
  119. local c = str:sub( i, i )
  120. if c == '\n' then lines = lines + 1 end
  121. end
  122. return lines
  123. end
  124. local function WindowExists( id )
  125. for i, v in ipairs( windows ) do
  126. if v.id == id then
  127. return true, i
  128. end
  129. end
  130. return false, 0
  131. end
  132. local function PointInRect( px, py, rx, ry, rw, rh )
  133. if px >= rx and px <= rx + rw and py >= ry and py <= ry + rh then
  134. return true
  135. end
  136. return false
  137. end
  138. local function MapRange( from_min, from_max, to_min, to_max, v )
  139. return (v - from_min) * (to_max - to_min) / (from_max - from_min) + to_min
  140. end
  141. local function GetLabelPart( name )
  142. local i = string.find( name, "##" )
  143. if i then
  144. return string.sub( name, 1, i - 1 )
  145. end
  146. return name
  147. end
  148. local function ResetLayout()
  149. layout = { x = 0, y = 0, w = 0, h = 0, row_h = 0, total_w = 0, total_h = 0, same_line = false, same_column = false }
  150. end
  151. local function UpdateLayout( bbox )
  152. -- Update row height
  153. if layout.same_line then
  154. if bbox.h > layout.row_h then
  155. layout.row_h = bbox.h
  156. end
  157. elseif layout.same_column then
  158. if bbox.h + layout.h + margin < layout.row_h then
  159. layout.row_h = layout.row_h - layout.h - margin
  160. else
  161. layout.row_h = bbox.h
  162. end
  163. else
  164. layout.row_h = bbox.h
  165. end
  166. -- Calculate current layout w/h
  167. if bbox.x + bbox.w + margin > layout.total_w then
  168. layout.total_w = bbox.x + bbox.w + margin
  169. end
  170. if bbox.y + layout.row_h + margin > layout.total_h then
  171. layout.total_h = bbox.y + layout.row_h + margin
  172. end
  173. -- Update layout x/y/w/h and same_line
  174. layout.x = bbox.x
  175. layout.y = bbox.y
  176. layout.w = bbox.w
  177. layout.h = bbox.h
  178. layout.same_line = false
  179. layout.same_column = false
  180. end
  181. local function Slider( type, name, v, v_min, v_max, width )
  182. local text = GetLabelPart( name )
  183. local cur_window = windows[ begin_idx ]
  184. local text_w = font.handle:getWidth( text )
  185. local slider_w = 10 * font.w
  186. local bbox = {}
  187. if layout.same_line then
  188. bbox = { x = layout.x + layout.w + margin, y = layout.y, w = slider_w + margin + text_w, h = (2 * margin) + font.h }
  189. elseif layout.same_column then
  190. bbox = { x = layout.x, y = layout.y + layout.h + margin, w = slider_w + margin + text_w, h = (2 * margin) + font.h }
  191. else
  192. bbox = { x = margin, y = layout.y + layout.row_h + margin, w = slider_w + margin + text_w, h = (2 * margin) + font.h }
  193. end
  194. if width and width > bbox.w then
  195. bbox.w = width
  196. slider_w = width - margin - text_w
  197. end
  198. UpdateLayout( bbox )
  199. local col = colors.slider_bg
  200. local result = false
  201. if not modal_window or (modal_window and modal_window == cur_window.id) then
  202. 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
  203. col = colors.slider_bg_hover
  204. if mouse.state == e_mouse_state.clicked then
  205. active_widget = cur_window.id .. name
  206. end
  207. end
  208. end
  209. if mouse.state == e_mouse_state.held and active_widget == cur_window.id .. name and cur_window == active_window then
  210. v = MapRange( bbox.x + 2, bbox.x + slider_w - 2, v_min, v_max, mouse.x - cur_window.x )
  211. if type == e_slider_type.float then
  212. v = Clamp( v, v_min, v_max )
  213. else
  214. v = Clamp( math.ceil( v ), v_min, v_max )
  215. if v == 0 then v = 0 end
  216. end
  217. end
  218. if mouse.state == e_mouse_state.released and active_widget == cur_window.id .. name then
  219. active_widget = nil
  220. result = true
  221. end
  222. local value_text_w = font.handle:getWidth( v )
  223. local text_label_rect = { x = bbox.x + slider_w + margin, y = bbox.y, w = text_w, h = bbox.h }
  224. local text_value_rect = { x = bbox.x, y = bbox.y, w = slider_w, h = bbox.h }
  225. local slider_rect = { x = bbox.x, y = bbox.y + (bbox.h / 2) - (font.h / 2), w = slider_w, h = font.h }
  226. local thumb_pos = MapRange( v_min, v_max, bbox.x, bbox.x + slider_w - font.h, v )
  227. local thumb_rect = { x = thumb_pos, y = bbox.y + (bbox.h / 2) - (font.h / 2), w = font.h, h = font.h }
  228. local value
  229. if type == e_slider_type.float then
  230. num_decimals = num_decimals or 2
  231. local str_fmt = "%." .. num_decimals .. "f"
  232. value = string.format( str_fmt, v )
  233. else
  234. value = v
  235. end
  236. table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = slider_rect, color = col } )
  237. table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = thumb_rect, color = colors.slider_thumb } )
  238. table.insert( windows[ begin_idx ].command_list, { type = "text", text = text, bbox = text_label_rect, color = colors.text } )
  239. table.insert( windows[ begin_idx ].command_list, { type = "text", text = value, bbox = text_value_rect, color = colors.text } )
  240. return result, v
  241. end
  242. function utf8.sub( s, i, j )
  243. i = utf8.offset( s, i )
  244. j = utf8.offset( s, j + 1 ) - 1
  245. return string.sub( s, i, j )
  246. end
  247. function lovr.textinput( text, code )
  248. text_input_character = text
  249. -- print( "here")
  250. end
  251. function lovr.keypressed( key, scancode, repeating )
  252. if repeating then
  253. if key == "right" then
  254. repeating_key = "right"
  255. elseif key == "left" then
  256. repeating_key = "left"
  257. elseif key == "backspace" then
  258. repeating_key = "backspace"
  259. end
  260. end
  261. end
  262. function lovr.keyreleased( key, scancode )
  263. repeating_key = nil
  264. end
  265. ---------------------------------------------------------------
  266. function UI2D.Init( size )
  267. font.handle = lovr.graphics.newFont( "ui2d/" .. "DejaVuSansMono.ttf", size or 14, 4 )
  268. font.handle:setPixelDensity( 1.0 )
  269. font.h = font.handle:getHeight()
  270. font.w = font.handle:getWidth( "W" )
  271. lovr.system.setKeyRepeat( true )
  272. end
  273. function UI2D.InputInfo()
  274. -- Get mouse
  275. if lovr.system.isMouseDown( 1 ) then
  276. if mouse.prev_frame == 0 then
  277. mouse.prev_frame = 1
  278. mouse.this_frame = 1
  279. mouse.state = e_mouse_state.clicked
  280. else
  281. mouse.prev_frame = 1
  282. mouse.this_frame = 0
  283. mouse.state = e_mouse_state.held
  284. end
  285. else
  286. if mouse.prev_frame == 1 then
  287. mouse.state = e_mouse_state.released
  288. mouse.prev_frame = 0
  289. else
  290. mouse.state = e_mouse_state.idle
  291. end
  292. end
  293. mouse.x, mouse.y = lovr.system.getMousePosition()
  294. -- Set active window on click
  295. local hovers_active = false
  296. local hovers_any = false
  297. for i, v in ipairs( windows ) do
  298. if PointInRect( mouse.x, mouse.y, v.x, v.y, v.w, v.h ) then
  299. if v == active_window then
  300. hovers_active = true
  301. end
  302. hovers_any = true
  303. end
  304. end
  305. if not hovers_active then
  306. for i, v in ipairs( windows ) do
  307. if PointInRect( mouse.x, mouse.y, v.x, v.y, v.w, v.h ) and mouse.state == e_mouse_state.clicked then
  308. active_window = v
  309. end
  310. end
  311. end
  312. -- Set active to none
  313. if not hovers_any and mouse.state == e_mouse_state.clicked then
  314. active_window = nil
  315. end
  316. -- Handle window dragging
  317. if active_window then
  318. local v = active_window
  319. if PointInRect( mouse.x, mouse.y, v.x, v.y, v.w, (2 * margin) + font.h ) and mouse.state == e_mouse_state.clicked then
  320. dragged_window = active_window
  321. dragged_window_offset.x = mouse.x - active_window.x
  322. dragged_window_offset.y = mouse.y - active_window.y
  323. end
  324. if dragged_window then
  325. if mouse.state == e_mouse_state.held then
  326. dragged_window.x = mouse.x - dragged_window_offset.x
  327. dragged_window.y = mouse.y - dragged_window_offset.y
  328. end
  329. end
  330. end
  331. if mouse.state == e_mouse_state.released then
  332. dragged_window = nil
  333. end
  334. end
  335. function UI2D.Begin( name, x, y, is_modal )
  336. local exists, idx = WindowExists( name ) -- TODO: Can't currently change window title on runtime
  337. if not exists then
  338. local window = {
  339. id = name,
  340. title = GetLabelPart( name ),
  341. x = x,
  342. y = y,
  343. w = 0,
  344. h = 0,
  345. command_list = {},
  346. texture = nil,
  347. texture_w = 0,
  348. texture_h = 0,
  349. pass = nil,
  350. is_hovered = false,
  351. is_modal = is_modal or false
  352. }
  353. table.insert( windows, window )
  354. end
  355. layout.y = (2 * margin) + font.h
  356. if idx == 0 then
  357. begin_idx = #windows
  358. else
  359. begin_idx = idx
  360. end
  361. end
  362. function UI2D.End( main_pass )
  363. local cur_window = windows[ begin_idx ]
  364. cur_window.w = layout.total_w
  365. cur_window.h = layout.total_h
  366. assert( cur_window.w > 0, "Begin/End block without widgets!" )
  367. -- Cache texture
  368. if cur_window.texture then
  369. if cur_window.texture_w ~= cur_window.w or cur_window.texture_h ~= cur_window.h then
  370. cur_window.texture:release()
  371. cur_window.texture_w = cur_window.w
  372. cur_window.texture_h = cur_window.h
  373. cur_window.texture = lovr.graphics.newTexture( cur_window.w, cur_window.h, texture_flags )
  374. cur_window.pass:setCanvas( cur_window.texture )
  375. end
  376. else
  377. cur_window.texture = lovr.graphics.newTexture( cur_window.w, cur_window.h, texture_flags )
  378. cur_window.texture_w = cur_window.w
  379. cur_window.texture_h = cur_window.h
  380. cur_window.pass = lovr.graphics.newPass( cur_window.texture )
  381. end
  382. cur_window.pass:reset()
  383. cur_window.pass:setFont( font.handle )
  384. cur_window.pass:setDepthTest( nil )
  385. cur_window.pass:setProjection( 1, mat4():orthographic( cur_window.pass:getDimensions() ) )
  386. cur_window.pass:setColor( colors.window_bg )
  387. cur_window.pass:fill()
  388. -- Title bar and border
  389. local title_col = colors.window_titlebar
  390. if cur_window == active_window then
  391. title_col = colors.window_titlebar_active
  392. end
  393. table.insert( windows[ begin_idx ].command_list,
  394. { type = "rect_fill", bbox = { x = 0, y = 0, w = cur_window.w, h = (2 * margin) + font.h }, color = title_col } )
  395. local txt = cur_window.title
  396. local title_w = utf8.len( txt ) * font.w
  397. if title_w > cur_window.w - (2 * margin) then -- Truncate title
  398. local num_chars = ((cur_window.w - (2 * margin)) / font.w) - 3
  399. txt = string.sub( txt, 1, num_chars ) .. "..."
  400. title_w = utf8.len( txt ) * font.w
  401. end
  402. table.insert( windows[ begin_idx ].command_list,
  403. { type = "text", text = txt, bbox = { x = margin, y = 0, w = title_w, h = (2 * margin) + font.h }, color = colors.text } )
  404. table.insert( windows[ begin_idx ].command_list,
  405. { type = "rect_wire", bbox = { x = 0, y = 0, w = cur_window.w, h = cur_window.h }, color = colors.window_border } )
  406. -- Do draw commands
  407. for i, v in ipairs( cur_window.command_list ) do
  408. if v.type == "rect_fill" then
  409. if v.is_separator then
  410. cur_window.pass:setColor( v.color )
  411. cur_window.pass:plane( v.bbox.x + (cur_window.w / 2), v.bbox.y, 0, cur_window.w - (2 * margin), separator_thickness, 0, 0, 0, 0, "fill" )
  412. else
  413. cur_window.pass:setColor( v.color )
  414. cur_window.pass:plane( v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), 0, v.bbox.w, v.bbox.h, 0, 0, 0, 0, "fill" )
  415. end
  416. elseif v.type == "rect_wire" then
  417. local m = lovr.math.newMat4( vec3( v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), 0 ), vec3( v.bbox.w, v.bbox.h, 0 ) )
  418. cur_window.pass:setColor( v.color )
  419. cur_window.pass:plane( m, "line" )
  420. elseif v.type == "circle_wire" then
  421. local m = lovr.math.newMat4( vec3( v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), 0 ), vec3( v.bbox.w / 2, v.bbox.h / 2, 0 ) )
  422. cur_window.pass:setColor( v.color )
  423. cur_window.pass:circle( m, "line" )
  424. elseif v.type == "circle_fill" then
  425. local m = lovr.math.newMat4( vec3( v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), 0 ), vec3( v.bbox.w / 3, v.bbox.h / 3, 0 ) )
  426. cur_window.pass:setColor( v.color )
  427. cur_window.pass:circle( m, "fill" )
  428. elseif v.type == "text" then
  429. cur_window.pass:setColor( v.color )
  430. cur_window.pass:text( v.text, vec3( v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), 0 ) )
  431. elseif v.type == "image" then
  432. -- NOTE Temp fix. Had to do negative vertical scale. Otherwise image gets flipped?
  433. local m = lovr.math.newMat4( vec3( v.bbox.x + (v.bbox.w / 2), v.bbox.y + (v.bbox.h / 2), 0 ), vec3( v.bbox.w, -v.bbox.h, 0 ) )
  434. cur_window.pass:setColor( v.color )
  435. cur_window.pass:setMaterial( v.texture )
  436. cur_window.pass:setSampler( clamp_sampler )
  437. cur_window.pass:plane( m, "fill" )
  438. cur_window.pass:setMaterial()
  439. cur_window.pass:setColor( 1, 1, 1 )
  440. end
  441. end
  442. main_pass:setColor( 1, 1, 1 )
  443. main_pass:setMaterial( cur_window.texture )
  444. local z = 1
  445. if cur_window == active_window then z = 0 end
  446. main_pass:plane( cur_window.x + (cur_window.w / 2), cur_window.y + (cur_window.h / 2), z, cur_window.w, -cur_window.h ) --NOTE flip Y fix
  447. main_pass:setMaterial()
  448. ResetLayout()
  449. end
  450. function UI2D.SetWindowPosition( id, x, y )
  451. local exists, idx = WindowExists( id )
  452. if exists then
  453. windows[ idx ].x = x
  454. windows[ idx ].y = y
  455. return true
  456. end
  457. return false
  458. end
  459. function UI2D.SetColorTheme( theme, copy_from )
  460. if type( theme ) == "string" then
  461. colors = color_themes[ theme ]
  462. elseif type( theme ) == "table" then
  463. copy_from = copy_from or "dark"
  464. for i, v in pairs( color_themes[ copy_from ] ) do
  465. if theme[ i ] == nil then
  466. theme[ i ] = v
  467. end
  468. end
  469. colors = theme
  470. end
  471. end
  472. function UI2D.GetColorTheme()
  473. for i, v in pairs( color_themes ) do
  474. if v == colors then
  475. return i
  476. end
  477. end
  478. end
  479. function UI2D.OverrideColor( col_name, color )
  480. if not overriden_colors[ col_name ] then
  481. local old_color = colors[ col_name ]
  482. overriden_colors[ col_name ] = old_color
  483. colors[ col_name ] = color
  484. end
  485. end
  486. function UI2D.ResetColor( col_name )
  487. if overriden_colors[ col_name ] then
  488. colors[ col_name ] = overriden_colors[ col_name ]
  489. overriden_colors[ col_name ] = nil
  490. end
  491. end
  492. function UI2D.SameLine()
  493. layout.same_line = true
  494. end
  495. function UI2D.SameColumn()
  496. layout.same_column = true
  497. end
  498. function UI2D.Button( name, width, height )
  499. local text = GetLabelPart( name )
  500. local cur_window = windows[ begin_idx ]
  501. local text_w = utf8.len( name ) * font.w
  502. local num_lines = GetLineCount( name )
  503. local bbox = {}
  504. if layout.same_line then
  505. bbox = { x = layout.x + layout.w + margin, y = layout.y, w = (2 * margin) + text_w, h = (2 * margin) + (num_lines * font.h) }
  506. elseif layout.same_column then
  507. bbox = { x = layout.x, y = layout.y + layout.h + margin, w = (2 * margin) + text_w, h = (2 * margin) + (num_lines * font.h) }
  508. else
  509. bbox = { x = margin, y = layout.y + layout.row_h + margin, w = (2 * margin) + text_w, h = (2 * margin) + (num_lines * font.h) }
  510. end
  511. if width and type( width ) == "number" and width > bbox.w then
  512. bbox.w = width
  513. end
  514. if height and type( height ) == "number" and height > bbox.h then
  515. bbox.h = height
  516. end
  517. UpdateLayout( bbox )
  518. local result = false
  519. local col = colors.button_bg
  520. if not modal_window or (modal_window and modal_window == cur_window.id) then
  521. 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
  522. col = colors.button_bg_hover
  523. if mouse.state == e_mouse_state.clicked then
  524. result = true
  525. end
  526. if mouse.state == e_mouse_state.held then
  527. col = colors.button_bg_click
  528. end
  529. end
  530. end
  531. table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = bbox, color = col } )
  532. table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = bbox, color = colors.button_border } )
  533. table.insert( windows[ begin_idx ].command_list, { type = "text", text = text, bbox = bbox, color = colors.text } )
  534. return result
  535. end
  536. function UI2D.SliderInt( name, v, v_min, v_max, width )
  537. return Slider( e_slider_type.int, name, v, v_min, v_max, width )
  538. end
  539. function UI2D.SliderFloat( name, v, v_min, v_max, width, num_decimals )
  540. return Slider( e_slider_type.float, name, v, v_min, v_max, width, num_decimals )
  541. end
  542. function UI2D.ProgressBar( progress, width )
  543. if width and width >= (2 * margin) + (4 * font.w) then
  544. width = width
  545. else
  546. width = 300
  547. end
  548. local bbox = {}
  549. if layout.same_line then
  550. bbox = { x = layout.x + layout.w + margin, y = layout.y, w = width, h = (2 * margin) + font.h }
  551. elseif layout.same_column then
  552. bbox = { x = layout.x, y = layout.y + layout.h, w = width, h = (2 * margin) + font.h }
  553. else
  554. bbox = { x = margin, y = layout.y + layout.row_h + margin, w = width, h = (2 * margin) + font.h }
  555. end
  556. UpdateLayout( bbox )
  557. progress = Clamp( progress, 0, 100 )
  558. local fill_w = math.floor( (width * progress) / 100 )
  559. local str = progress .. "%"
  560. table.insert( windows[ begin_idx ].command_list,
  561. { type = "rect_fill", bbox = { x = bbox.x, y = bbox.y, w = fill_w, h = bbox.h }, color = colors.progress_bar_fill } )
  562. table.insert( windows[ begin_idx ].command_list,
  563. { 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 } )
  564. table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = bbox, color = colors.progress_bar_border } )
  565. table.insert( windows[ begin_idx ].command_list, { type = "text", text = str, bbox = bbox, color = colors.text } )
  566. end
  567. function UI2D.Separator()
  568. local bbox = {}
  569. if layout.same_line or layout.same_column then
  570. return
  571. else
  572. bbox = { x = 0, y = layout.y + layout.row_h + margin, w = 0, h = 0 }
  573. end
  574. UpdateLayout( bbox )
  575. table.insert( windows[ begin_idx ].command_list, { is_separator = true, type = "rect_fill", bbox = bbox, color = colors.separator } )
  576. end
  577. function UI2D.ImageButton( texture, width, height, text )
  578. local cur_window = windows[ begin_idx ]
  579. local width = width or texture:getWidth()
  580. local height = height or texture:getHeight()
  581. local bbox = {}
  582. if layout.same_line then
  583. bbox = { x = layout.x + layout.w + margin, y = layout.y, w = width, h = height }
  584. elseif layout.same_column then
  585. bbox = { x = layout.x, y = layout.y + layout.h + margin, w = width, height = height }
  586. else
  587. bbox = { x = margin, y = layout.y + layout.row_h + margin, w = width, h = height }
  588. end
  589. local text_w
  590. if text then
  591. text_w = font.handle:getWidth( text )
  592. font.h = font.handle:getHeight()
  593. if font.h > bbox.h then
  594. bbox.h = font.h
  595. end
  596. bbox.w = bbox.w + (2 * margin) + text_w
  597. end
  598. UpdateLayout( bbox )
  599. local result = false
  600. local col = 1
  601. if not modal_window or (modal_window and modal_window == cur_window.id) then
  602. 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
  603. table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = bbox, color = colors.image_button_border_highlight } )
  604. if mouse.state == e_mouse_state.clicked then
  605. result = true
  606. end
  607. if mouse.state == e_mouse_state.held then
  608. col = 0.7
  609. end
  610. end
  611. end
  612. if text then
  613. table.insert( windows[ begin_idx ].command_list,
  614. { type = "image", bbox = { x = bbox.x, y = bbox.y + ((bbox.h - height) / 2), w = width, h = height }, texture = texture, color = { col, col, col } } )
  615. table.insert( windows[ begin_idx ].command_list,
  616. { type = "text", text = text, bbox = { x = bbox.x + width, y = bbox.y, w = text_w + (2 * margin), h = bbox.h }, color = colors.text } )
  617. else
  618. table.insert( windows[ begin_idx ].command_list, { type = "image", bbox = bbox, texture = texture, color = { col, col, col } } )
  619. end
  620. return result
  621. end
  622. function UI2D.Dummy( width, height )
  623. local bbox = {}
  624. if layout.same_line then
  625. bbox = { x = layout.x + layout.w + margin, y = layout.y, w = width, h = height }
  626. else
  627. bbox = { x = margin, y = layout.y + layout.row_h + margin, w = width, h = height }
  628. end
  629. UpdateLayout( bbox )
  630. end
  631. function UI2D.TabBar( name, tabs, idx )
  632. local cur_window = windows[ begin_idx ]
  633. local bbox = {}
  634. if layout.same_line then
  635. bbox = { x = layout.x + layout.w + margin, y = layout.y, w = 0, h = (2 * margin) + font.h }
  636. else
  637. bbox = { x = margin, y = layout.y + layout.row_h + margin, w = 0, h = (2 * margin) + font.h }
  638. end
  639. local result = false, idx
  640. local total_w = 0
  641. local col = colors.tab_bar_bg
  642. local x_off = bbox.x
  643. for i, v in ipairs( tabs ) do
  644. local text_w = font.handle:getWidth( v )
  645. local tab_w = text_w + (2 * margin)
  646. bbox.w = bbox.w + tab_w
  647. if not modal_window or (modal_window and modal_window == cur_window.id) then
  648. 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
  649. col = colors.tab_bar_hover
  650. if mouse.state == e_mouse_state.clicked then
  651. idx = i
  652. result = true
  653. end
  654. else
  655. col = colors.tab_bar_bg
  656. end
  657. end
  658. local tab_rect = { x = x_off, y = bbox.y, w = tab_w, h = bbox.h }
  659. table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = tab_rect, color = col } )
  660. table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = tab_rect, color = colors.tab_bar_border } )
  661. table.insert( windows[ begin_idx ].command_list, { type = "text", text = v, bbox = tab_rect, color = colors.text } )
  662. if idx == i then
  663. -- table.insert( windows[ begin_idx ].command_list,
  664. -- { type = "rect_fill", bbox = { x = tab_rect.x + 2, y = tab_rect.y + tab_rect.h - 6, w = tab_rect.w - 4, h = 5 }, color = colors.tab_bar_highlight } )
  665. local highlight_thickness = math.floor( font.h / 4 )
  666. table.insert( windows[ begin_idx ].command_list,
  667. {
  668. type = "rect_fill",
  669. bbox = { x = tab_rect.x + 2, y = tab_rect.y + tab_rect.h - (highlight_thickness), w = tab_rect.w - 4, h = highlight_thickness },
  670. color = colors.tab_bar_highlight
  671. } )
  672. end
  673. x_off = x_off + tab_w
  674. end
  675. table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = bbox, color = colors.tab_bar_border } )
  676. UpdateLayout( bbox )
  677. return result, idx
  678. end
  679. function UI2D.Label( text, compact )
  680. local text_w = font.handle:getWidth( text )
  681. local num_lines = GetLineCount( text )
  682. local mrg = (2 * margin)
  683. if compact then
  684. mrg = 0
  685. end
  686. local bbox = {}
  687. if layout.same_line then
  688. bbox = { x = layout.x + layout.w + margin, y = layout.y, w = text_w, h = mrg + (num_lines * font.h) }
  689. elseif layout.same_column then
  690. bbox = { x = layout.x, y = layout.y + layout.h + margin, w = text_w, h = mrg + (num_lines * font.h) }
  691. else
  692. bbox = { x = margin, y = layout.y + layout.row_h + margin, w = text_w, h = mrg + (num_lines * font.h) }
  693. end
  694. UpdateLayout( bbox )
  695. table.insert( windows[ begin_idx ].command_list, { type = "text", text = text, bbox = bbox, color = colors.text } )
  696. end
  697. function UI2D.CheckBox( text, checked )
  698. local cur_window = windows[ begin_idx ]
  699. local text_w = font.handle:getWidth( text )
  700. local bbox = {}
  701. if layout.same_line then
  702. bbox = { x = layout.x + layout.w + margin, y = layout.y, w = font.h + margin + text_w, h = (2 * margin) + font.h }
  703. else
  704. bbox = { x = margin, y = layout.y + layout.row_h + margin, w = font.h + margin + text_w, h = (2 * margin) + font.h }
  705. end
  706. UpdateLayout( bbox )
  707. local result = false
  708. local col = colors.check_border
  709. if not modal_window or (modal_window and modal_window == cur_window.id) then
  710. 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
  711. col = colors.check_border_hover
  712. if mouse.state == e_mouse_state.clicked then
  713. result = true
  714. end
  715. end
  716. end
  717. local check_rect = { x = bbox.x, y = bbox.y + margin, w = font.h, h = font.h }
  718. local text_rect = { x = bbox.x + font.h + margin, y = bbox.y, w = text_w + margin, h = bbox.h }
  719. table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = check_rect, color = col } )
  720. table.insert( windows[ begin_idx ].command_list, { type = "text", text = text, bbox = text_rect, color = colors.text } )
  721. if checked and type( checked ) == "boolean" then
  722. table.insert( windows[ begin_idx ].command_list, { type = "text", text = "✔", bbox = check_rect, color = colors.check_mark } )
  723. end
  724. return result
  725. end
  726. function UI2D.RadioButton( text, checked )
  727. local cur_window = windows[ begin_idx ]
  728. local text_w = font.handle:getWidth( text )
  729. local bbox = {}
  730. if layout.same_line then
  731. bbox = { x = layout.x + layout.w + margin, y = layout.y, w = font.h + margin + text_w, h = (2 * margin) + font.h }
  732. else
  733. bbox = { x = margin, y = layout.y + layout.row_h + margin, w = font.h + margin + text_w, h = (2 * margin) + font.h }
  734. end
  735. UpdateLayout( bbox )
  736. local result = false
  737. local col = colors.radio_border
  738. if not modal_window or (modal_window and modal_window == cur_window.id) then
  739. 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
  740. col = colors.radio_border_hover
  741. if mouse.state == e_mouse_state.clicked then
  742. result = true
  743. end
  744. end
  745. end
  746. local check_rect = { x = bbox.x, y = bbox.y + margin, w = font.h, h = font.h }
  747. local text_rect = { x = bbox.x + font.h + margin, y = bbox.y, w = text_w + margin, h = bbox.h }
  748. table.insert( windows[ begin_idx ].command_list, { type = "circle_wire", bbox = check_rect, color = col } )
  749. table.insert( windows[ begin_idx ].command_list, { type = "text", text = text, bbox = text_rect, color = colors.text } )
  750. if checked and type( checked ) == "boolean" then
  751. table.insert( windows[ begin_idx ].command_list, { type = "circle_fill", bbox = check_rect, color = colors.radio_mark } )
  752. end
  753. return result
  754. end
  755. function UI2D.TextBox( name, num_visible_chars, text )
  756. local cur_window = windows[ begin_idx ]
  757. local label_w = font.handle:getWidth( name )
  758. local bbox = {}
  759. if layout.same_line then
  760. 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 }
  761. else
  762. 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 }
  763. end
  764. UpdateLayout( bbox )
  765. local scroll = 0
  766. if active_textbox then
  767. scroll = active_textbox.scroll
  768. end
  769. local text_rect = { x = bbox.x, y = bbox.y, w = (2 * margin) + (num_visible_chars * font.w), h = bbox.h }
  770. local visible_text = nil
  771. if utf8.len( text ) > num_visible_chars then
  772. visible_text = utf8.sub( text, scroll + 1, scroll + num_visible_chars )
  773. else
  774. visible_text = text
  775. end
  776. local label_rect = { x = text_rect.x + text_rect.w + margin, y = bbox.y, w = label_w, h = bbox.h }
  777. local char_rect = { x = text_rect.x + margin, y = text_rect.y, w = (utf8.len( visible_text ) * font.w), h = text_rect.h }
  778. -- Caret
  779. local caret_rect = nil
  780. if active_widget == cur_window.id .. name then
  781. if text_input_character then
  782. local p = active_textbox.caret + active_textbox.scroll
  783. local part1 = utf8.sub( text, 1, p )
  784. local part2 = utf8.sub( text, p + 1, utf8.len( text ) )
  785. text = part1 .. text_input_character .. part2
  786. active_textbox.caret = active_textbox.caret + 1
  787. if active_textbox.caret > num_visible_chars then
  788. active_textbox.scroll = active_textbox.scroll + 1
  789. end
  790. end
  791. if lovr.system.wasKeyPressed( "backspace" ) or repeating_key == "backspace" then
  792. if active_textbox.caret > 0 then
  793. local p = active_textbox.caret + active_textbox.scroll
  794. local part1 = utf8.sub( text, 1, p - 1 )
  795. local part2 = utf8.sub( text, p + 1, utf8.len( text ) )
  796. text = part1 .. part2
  797. local max_scroll = utf8.len( text ) - num_visible_chars
  798. -- if active_textbox.scroll < max_scroll or utf8.len( text ) <= num_visible_chars then
  799. if active_textbox.scroll < max_scroll or utf8.len( text ) < num_visible_chars then
  800. active_textbox.caret = active_textbox.caret - 1
  801. end
  802. -- if active_textbox.scroll <= 0 then
  803. -- active_textbox.caret = active_textbox.caret - 1
  804. -- end
  805. -- if active_textbox.scroll > max_scroll then
  806. -- active_textbox.scroll = active_textbox.scroll - 1
  807. -- end
  808. end
  809. end
  810. if lovr.system.wasKeyPressed( "left" ) or repeating_key == "left" then
  811. if active_textbox.caret == 0 then
  812. if active_textbox.scroll > 0 then
  813. active_textbox.scroll = active_textbox.scroll - 1
  814. end
  815. end
  816. active_textbox.caret = active_textbox.caret - 1
  817. end
  818. if lovr.system.wasKeyPressed( "right" ) or repeating_key == "right" then
  819. local full_length = utf8.len( text )
  820. local visible_length = utf8.len( visible_text )
  821. if active_textbox.caret == num_visible_chars and full_length > num_visible_chars and active_textbox.scroll < (full_length - visible_length) then
  822. active_textbox.scroll = active_textbox.scroll + 1
  823. end
  824. if active_textbox.caret < full_length then
  825. active_textbox.caret = active_textbox.caret + 1
  826. end
  827. end
  828. local max_scroll = utf8.len( text ) - num_visible_chars
  829. if max_scroll < 0 then max_scroll = 0 end
  830. active_textbox.scroll = Clamp( active_textbox.scroll, 0, max_scroll )
  831. scroll = active_textbox.scroll
  832. active_textbox.caret = Clamp( active_textbox.caret, 0, num_visible_chars )
  833. caret_rect = { x = char_rect.x + (active_textbox.caret * font.w), y = char_rect.y + margin, w = 2, h = font.h }
  834. end
  835. local col1 = colors.textbox_bg
  836. local col2 = colors.textbox_border
  837. if not modal_window or (modal_window and modal_window == cur_window.id) then
  838. 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
  839. col1 = colors.textbox_bg_hover
  840. if mouse.state == e_mouse_state.clicked then
  841. local pos = math.floor( (mouse.x - cur_window.x - text_rect.x) / font.w )
  842. if pos > utf8.len( text ) then
  843. pos = utf8.len( text )
  844. end
  845. if active_widget ~= cur_window.id .. name then
  846. active_textbox = { id = cur_window.id .. name, caret = pos }
  847. active_textbox.scroll = 0
  848. active_widget = cur_window.id .. name
  849. else
  850. active_textbox.caret = pos
  851. end
  852. end
  853. else
  854. if mouse.state == e_mouse_state.clicked then
  855. if active_widget == cur_window.id .. name then -- Deactivate self
  856. active_textbox = nil
  857. active_widget = nil
  858. return text
  859. end
  860. end
  861. end
  862. if active_widget == cur_window.id .. name then
  863. if lovr.system.wasKeyPressed( "tab" ) or lovr.system.wasKeyPressed( "return" ) then -- Deactivate self
  864. active_textbox = nil
  865. active_widget = nil
  866. return text
  867. end
  868. end
  869. end
  870. table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = text_rect, color = col1 } )
  871. table.insert( windows[ begin_idx ].command_list, { type = "rect_wire", bbox = text_rect, color = col2 } )
  872. table.insert( windows[ begin_idx ].command_list, { type = "text", text = visible_text, bbox = char_rect, color = colors.text } )
  873. table.insert( windows[ begin_idx ].command_list, { type = "text", text = name, bbox = label_rect, color = colors.text } )
  874. if caret_rect then
  875. table.insert( windows[ begin_idx ].command_list, { type = "rect_fill", bbox = caret_rect, color = colors.text } )
  876. end
  877. return text
  878. end
  879. function UI2D.ListBox( name )
  880. end
  881. function UI2D.NewFrame( main_pass )
  882. font.handle:setPixelDensity( 1.0 )
  883. end
  884. function UI2D.RenderFrame( main_pass )
  885. text_input_character = nil
  886. local passes = {}
  887. for i, v in ipairs( windows ) do
  888. v.command_list = nil
  889. v.command_list = {}
  890. table.insert( passes, v.pass )
  891. end
  892. return passes
  893. end
  894. return UI2D