chui.lua 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819
  1. -- chui: a set of VR UI push-to-operate components (no laser pointers)
  2. local m = {}
  3. local function vibrate(device, strength, duration, frequency)
  4. if device ~= 'mouse' and lovr.headset then
  5. lovr.headset.vibrate(device, strength, duration, frequency)
  6. end
  7. end
  8. local Q = 0.02 -- quant; all paddings and margins are its multiples
  9. local S = 0.05 -- size of widget actuators
  10. local button_roundness = 0.3
  11. local slider_roundness = 0.1
  12. m.palettes = { -- a built-in collection of UI color palettes
  13. --widget body color for OFF color for ON highlight color them letters back-panel color
  14. { cap = 0xf0f0fb, inactive = 0xa2a6c1, active = 0xa479c7, hover = 0xb9aecd, text = 0x41486c, panel = 0xf6f7fe },
  15. { cap = 0x291d22, inactive = 0x3b3235, active = 0xf9b18e, hover = 0x9d5550, text = 0xfae8bc, panel = 0x374549 },
  16. { cap = 0x3b3149, inactive = 0x5c6181, active = 0xd47563, hover = 0xecc197, text = 0xecece0, panel = 0x191822 },
  17. { cap = 0x313131, inactive = 0x6f564c, active = 0xff9300, hover = 0xfdc484, text = 0x827d6d, panel = 0xf6b511 },
  18. { cap = 0xffeecc, inactive = 0x00b9be, active = 0xf57d7d, hover = 0xffb0a3, text = 0x15788c, panel = 0x264452 },
  19. { cap = 0x413a42, inactive = 0x1f1f29, active = 0xe68056, hover = 0x596070, text = 0xeaf0d8, panel = 0x16181b },
  20. { cap = 0x392b35, inactive = 0x7a9c96, active = 0xffab53, hover = 0x486b7f, text = 0xdac1c1, panel = 0x5e747e },
  21. { cap = 0x100f13, inactive = 0x372437, active = 0xa05642, hover = 0x693540, text = 0xc7955c, panel = 0x1a0d1e },
  22. { cap = 0x2a2a2b, inactive = 0x454a4d, active = 0x5a9470, hover = 0x2f7571, text = 0x81b071, panel = 0x202020 },
  23. { cap = 0x212124, inactive = 0x464c54, active = 0x76add8, hover = 0x5b8087, text = 0xa3e7f0, panel = 0x2b3a49 },
  24. { cap = 0x2e3b43, inactive = 0x619094, active = 0xdcfdcb, hover = 0x5a9e89, text = 0x5fa6ac, panel = 0x9ac0ba },
  25. { cap = 0xdddddd, inactive = 0x566063, active = 0x8caab5, hover = 0xfdfaf8, text = 0x073336, panel = 0xf4f4f3 },
  26. { cap = 0xa5c09d, inactive = 0x82a775, active = 0x7fe2e5, hover = 0xedf4f2, text = 0x165c44, panel = 0x308e7f },
  27. { cap = 0xd7417f, inactive = 0x3b3235, active = 0x785ea0, hover = 0x74509d, text = 0xfcd8d8, panel = 0xfd6193 },
  28. { cap = 0xecf1e6, inactive = 0xa8a9b9, active = 0x67bdc8, hover = 0x7fdd8e, text = 0x2d2614, panel = 0xf4fefe },
  29. { cap = 0xf5fffc, inactive = 0xa6a6a6, active = 0x5dd276, hover = 0x78cfd0, text = 0x173b4e, panel = 0xf0f4f3 },
  30. { cap = 0x46425e, inactive = 0xb28e7c, active = 0xdd9e43, hover = 0x72677b, text = 0xddc2bd, panel = 0x81828e },
  31. }
  32. m.mouse_available = (not lovr.headset) or (not lovr.headset.getName())
  33. m.segments = 7 -- amount of geometry for roundrects and cylinders
  34. m.panels = {}
  35. m.widget_types = {}
  36. -- SPACER ---------------------------------------------------------------------
  37. m.spacer = {}
  38. m.spacer.defaults = {}
  39. table.insert(m.widget_types, 'spacer')
  40. function m.spacer:init()
  41. end
  42. function m.spacer:draw(pass, pose)
  43. end
  44. function m.spacer:update(dt, pointer, pointer_name)
  45. end
  46. -- LABEL ----------------------------------------------------------------------
  47. m.label = {}
  48. m.label.defaults = { text = '', text_scale = 1 }
  49. table.insert(m.widget_types, 'label')
  50. function m.label:init(options)
  51. self.text = options.text
  52. self.text_scale = options.text_scale
  53. end
  54. function m.label:draw(pass, pose)
  55. -- text
  56. pass:setColor(self.parent.palette.text)
  57. pass:text(self.text, 0, 0, Q, 0.2 * self.text_scale)
  58. end
  59. function m.label:update(dt, pointer, pointer_name)
  60. end
  61. -- BUTTON ---------------------------------------------------------------------
  62. m.button = {}
  63. m.button.defaults = { text = '', thickness = 0.3, callback = nil, held = nil, text_scale = 0.3 }
  64. table.insert(m.widget_types, 'button')
  65. function m.button:init(options)
  66. self.interactive = true
  67. self.hovered = false
  68. self.text = options.text
  69. self.callback = options.callback
  70. self.held = options.held
  71. self.thickness = options.thickness
  72. self.text_scale = options.text_scale
  73. self.depth = self.thickness
  74. end
  75. function m.button:draw(pass)
  76. -- body
  77. pass:setColor(
  78. (self.depth < self.thickness / 2 and self.parent.palette.active) or
  79. (self.hovered and self.parent.palette.hover) or
  80. self.parent.palette.cap)
  81. pass:roundrect(0, 0, self.depth / 2,
  82. self.span[1] - 2 * Q, self.span[2] - 2 * Q, self.depth - Q,
  83. 0, 0,1,0,
  84. button_roundness * 0.75, m.segments)
  85. -- frame
  86. pass:setColor(self.parent.palette.inactive)
  87. pass:roundrect(0, 0, Q / 2,
  88. self.span[1], self.span[2], Q,
  89. 0, 0,1,0,
  90. button_roundness * 0.75, m.segments)
  91. -- text
  92. pass:setColor(self.parent.palette.text)
  93. pass:text(self.text, 0, 0, self.depth + Q, self.text_scale * self.span[2])
  94. end
  95. function m.button:update(dt, pointer, pointer_name)
  96. local new_depth = self.depth
  97. if pointer_name then -- pressing the button inward
  98. new_depth = math.min(self.thickness, math.max(2 * Q, pointer.z))
  99. end
  100. if pointer_name and self.hovered and -- button passed the threshold
  101. new_depth < self.thickness / 2 then
  102. if self.held then
  103. self.held(self)
  104. end
  105. if self.depth > self.thickness / 2 then
  106. vibrate(pointer_name, 0.2, 0.1)
  107. if self.callback then
  108. self.callback(self)
  109. end
  110. end
  111. end
  112. self.depth = new_depth
  113. self.hovered = pointer_name and true or false
  114. if not pointer_name then -- slowly rebound to above-hover depth when pointer leaves the widget
  115. self.depth = math.min(self.thickness, self.depth + 4 * dt)
  116. end
  117. end
  118. function m.button:get()
  119. return self.depth < self.thickness / 2
  120. end
  121. -- TOGGLE ---------------------------------------------------------------------
  122. m.toggle = {}
  123. m.toggle.defaults = { text = '', thickness = 0.3, state = false, callback = nil, text_scale = 0.3 }
  124. table.insert(m.widget_types, 'toggle')
  125. function m.toggle:init(options)
  126. self.interactive = true
  127. self.state = options.state
  128. self.hovered = false
  129. self.text = options.text
  130. self.callback = options.callback
  131. self.thickness = options.thickness
  132. self.text_scale = options.text_scale
  133. self.depth = self.thickness
  134. end
  135. function m.toggle:draw(pass)
  136. -- body
  137. pass:setColor(
  138. (self.state and self.parent.palette.active) or
  139. (self.hovered and self.parent.palette.hover) or
  140. self.parent.palette.cap)
  141. pass:roundrect(0, 0, self.depth / 2,
  142. self.span[1] - 2 * Q, self.span[2] - 2 * Q, self.depth - Q,
  143. 0, 0,1,0,
  144. button_roundness, m.segments)
  145. -- frame
  146. pass:setColor(self.parent.palette.inactive)
  147. pass:roundrect(0, 0, Q / 2,
  148. self.span[1], self.span[2], Q,
  149. 0, 0,1,0,
  150. button_roundness, m.segments)
  151. -- text
  152. pass:setColor(self.parent.palette.text)
  153. pass:text(self.text, 0, 0, self.depth + Q, self.text_scale * self.span[2])
  154. end
  155. function m.toggle:update(dt, pointer, pointer_name)
  156. local new_depth = self.depth
  157. if pointer_name then -- pressing the toggle inward
  158. new_depth = math.min(self.thickness, math.max(2 * Q, pointer.z))
  159. end
  160. if pointer_name and self.hovered and -- toggle button passed the threshold
  161. new_depth < self.thickness / 2 and
  162. self.depth > self.thickness / 2 then
  163. vibrate(pointer_name, 0.2, 0.1)
  164. self.state = not self.state
  165. if self.callback then
  166. self.callback(self, self.state)
  167. end
  168. end
  169. self.depth = new_depth
  170. self.hovered = pointer_name and true or false
  171. if not pointer_name then -- rebound
  172. self.depth = math.min(self.thickness, self.depth + 4 * dt)
  173. end
  174. end
  175. function m.toggle:get()
  176. return self.state
  177. end
  178. function m.toggle:set(state)
  179. self.state = state and true or false
  180. if self.callback then
  181. self.callback(self, self.state)
  182. end
  183. end
  184. -- GLOW -------------------------------------------------------------------------
  185. m.glow = {}
  186. m.glow.defaults = { text = '', thickness = 0.1, state = false, text_scale = 0.3 }
  187. table.insert(m.widget_types, 'glow')
  188. function m.glow:init(options)
  189. self.state = options.state
  190. self.text = options.text
  191. self.thickness = options.thickness
  192. self.text_scale = options.text_scale
  193. end
  194. function m.glow:draw(pass)
  195. -- body
  196. pass:setColor(
  197. (self.state and self.parent.palette.active) or
  198. self.parent.palette.inactive)
  199. pass:cylinder(0, 0, self.thickness / 2,
  200. 0.5, self.thickness,
  201. 0, 0,1,0, true, nil, nil, m.segments * 6)
  202. -- frame
  203. pass:setColor(self.parent.palette.inactive)
  204. pass:cylinder(0, 0, Q / 2,
  205. 0.5 + Q, Q,
  206. 0, 0,1,0, true, nil, nil, m.segments * 6)
  207. -- text
  208. pass:setColor(self.parent.palette.text)
  209. pass:text(self.text, 0, 0, self.thickness + Q, self.text_scale)
  210. end
  211. function m.glow:update(dt, pointer, pointer_name)
  212. end
  213. function m.glow:get()
  214. return self.state
  215. end
  216. function m.glow:set(state)
  217. self.state = state and true or false
  218. end
  219. -- PROGRESS ---------------------------------------------------------------------
  220. m.progress = {}
  221. m.progress.defaults = { text = '', value = 0, text_scale = 0.3 }
  222. table.insert(m.widget_types, 'progress')
  223. function m.progress:init(options)
  224. self.text = options.text
  225. self.text_scale = options.text_scale
  226. self:set(options.value)
  227. end
  228. function m.progress:draw(pass)
  229. -- value as horizontal bar
  230. local y = -0.15
  231. local aw = self.span[1] - S - 2 * Q -- available width
  232. local w = self.value * aw
  233. pass:setColor(self.parent.palette.text)
  234. pass:box(0, y, 2 * Q, aw - 2 * Q, 2 * S, S / 2)
  235. pass:setColor(self.parent.palette.active)
  236. pass:roundrect(-aw / 2 + w / 2, y, 4 * Q,
  237. w, 4 * S, 2 * S,
  238. 0, 0,1,0,
  239. 2 * Q, m.segments)
  240. -- text
  241. pass:setColor(self.parent.palette.text)
  242. pass:text(self.text, 0, 0.2, 2 * Q, self.text_scale)
  243. end
  244. function m.progress:get()
  245. return self.value
  246. end
  247. function m.progress:set(value)
  248. self.value = math.max(0, math.min(1, value))
  249. end
  250. function m.progress:update(dt, pointer, pointer_name)
  251. end
  252. -- SLIDER ---------------------------------------------------------------------
  253. m.slider = {}
  254. m.slider.__index = m.slider
  255. m.slider.defaults = { text = '', format = '%s %.2f',
  256. min = 0, max = 1, value = 0, step = nil,
  257. text_scale = 0.3, thickness = 0.15,
  258. callback = nil, live_update = true }
  259. table.insert(m.widget_types, 'slider')
  260. local function roundBy(value, step)
  261. local quant, frac = math.modf(value / step)
  262. return step * (quant + (frac > 0.5 and 1 or 0))
  263. end
  264. function m.slider:init(options)
  265. self.interactive = true
  266. self.text = options.text
  267. self.min = options.min
  268. self.max = options.max
  269. self.thickness = options.thickness
  270. self.text_scale = options.text_scale
  271. self.callback = options.callback
  272. self.step = options.step
  273. self.format = options.format
  274. self.live_update = options.live_update
  275. self.altered = false
  276. if not options.format and self.step then
  277. local digits = math.max(0, math.ceil(-math.log(self.step, 10)))
  278. self.format = string.format('%%s %%.%df', digits)
  279. end
  280. local value = options.value
  281. if self.step then
  282. value = roundBy(value, self.step)
  283. end
  284. self.value = math.max(self.min, math.min(self.max, value))
  285. end
  286. function m.slider:draw(pass)
  287. -- value knob
  288. local y = -0.15
  289. local aw = self.span[1] - S - 2 * Q -- available width
  290. local pos = (self.value - self.min) / (self.max - self.min) * aw
  291. pass:setColor(self.parent.palette.text)
  292. pass:box(0, y, 2 * Q, aw, 2 * S, S / 2)
  293. pass:setColor(self.parent.palette.active)
  294. pass:roundrect(-aw / 2 + pos, y, 2 * Q + self.thickness / 2,
  295. 2 * S, 6 * S, self.thickness,
  296. 0, 0,1,0,
  297. S, m.segments)
  298. -- frame
  299. pass:setColor(
  300. (self.altered and self.parent.palette.hover) or
  301. self.parent.palette.cap)
  302. pass:roundrect(0, 0, Q / 2,
  303. self.span[1], 1, Q,
  304. 0, 0,1,0,
  305. slider_roundness, m.segments)
  306. -- text
  307. pass:setColor(self.parent.palette.text)
  308. pass:text(string.format(self.format, self.text, self.value),
  309. 0, 0.2, 2 * Q, self.text_scale)
  310. end
  311. function m.slider:update(dt, pointer, pointer_name)
  312. local hovered = pointer_name and true or false
  313. local altered_next = pointer.z < self.thickness
  314. if hovered and altered_next then
  315. local aw = self.span[1] - 16 * Q -- available width
  316. local value = self.min + (aw / 2 + pointer.x) / aw * (self.max - self.min)
  317. self:set(value)
  318. vibrate(pointer_name, 0.2, dt)
  319. end
  320. if not altered_next and self.altered and self.callback then
  321. self.callback(self, self.value)
  322. end
  323. self.altered = altered_next
  324. end
  325. function m.slider:get()
  326. return self.value
  327. end
  328. function m.slider:set(value)
  329. if self.step then
  330. value = roundBy(value, self.step)
  331. end
  332. self.value = math.max(self.min, math.min(self.max, value))
  333. if self.callback and self.live_update then
  334. self.callback(self, self.value)
  335. end
  336. end
  337. -- PANEL ----------------------------------------------------------------------
  338. local panel = {}
  339. panel.__index = panel
  340. local panel_defaults = {
  341. frame = 'backpanel',
  342. palette = m.palettes[1],
  343. }
  344. function m.panel(options)
  345. options = options or {}
  346. local self = setmetatable({}, panel)
  347. self.is_panel = true
  348. self.frame = options.frame == nil and panel_defaults.frame or options.frame
  349. self.pose = Mat4(options.pose) -- the options.pose is allowed to be nil
  350. self.world_from_screen = Mat4()
  351. self.widgets = {}
  352. self.rows = {{}}
  353. self.span = {1, 1}
  354. self.palette = options.palette or panel_defaults.palette
  355. self.visible = true
  356. self.align_offset = Vec3()
  357. self.layout_options = {'center', 'center'}
  358. table.insert(m.panels, self)
  359. return self
  360. end
  361. function panel:reset()
  362. self.widgets = {}
  363. self.rows = {{}}
  364. self.align_offset:set(0, 0)
  365. end
  366. function panel:row()
  367. table.insert(self.rows, {})
  368. end
  369. function panel:nest(child_panel)
  370. assert(child_panel and type(child_panel) == 'table' and getmetatable(child_panel) == panel,
  371. '`child_panel` is not panel table', tostring(child_panel))
  372. child_panel.parent = self
  373. child_panel.widget_type = 'nested panel'
  374. self:appendWidget(child_panel)
  375. end
  376. local function scaledSpan(widget)
  377. local scale = 1
  378. if widget.is_panel then
  379. if widget.visible then
  380. scale = select(4, widget.pose:unpack())
  381. else
  382. scale = 0
  383. end
  384. end
  385. return widget.span[1] * scale, widget.span[2] * scale
  386. end
  387. -- set poses of contained widgets and calculate own span
  388. function panel:layout(horizontal_alignment, vertical_alignment)
  389. horizontal_alignment = horizontal_alignment or self.layout_options[1]
  390. vertical_alignment = vertical_alignment or self.layout_options[2]
  391. self.layout_options = {horizontal_alignment, vertical_alignment}
  392. local margin = 8 * Q -- margin between rows and widgets in row
  393. self.span[1] = 0
  394. self.span[2] = 0
  395. -- calculate total dimensions
  396. local row_heights = {}
  397. local row_widths = {}
  398. for r, row in ipairs(self.rows) do
  399. local max_height = 0
  400. local row_width = 0
  401. for c, widget in ipairs(row) do
  402. local hspan, vspan = scaledSpan(widget)
  403. max_height = math.max(max_height, vspan)
  404. row_width = row_width + hspan + (c < #row and margin or 0)
  405. end
  406. row_widths[r] = row_width
  407. self.span[1] = math.max(self.span[1], row_width)
  408. table.insert(row_heights, max_height)
  409. self.span[2] = self.span[2] + max_height + (r < #self.rows and margin or 0)
  410. end
  411. -- lay out all widgets across all rows
  412. local x_row, y_row
  413. y_row = self.span[2] / 2
  414. for r, row in ipairs(self.rows) do
  415. local max_height = row_heights[r]
  416. local row_width = row_widths[r]
  417. if horizontal_alignment == 'left' then
  418. x_row = -self.span[1] / 2
  419. elseif horizontal_alignment == 'right' then
  420. x_row = self.span[1] / 2 - row_width
  421. else
  422. x_row = -row_width / 2
  423. end
  424. local x = x_row
  425. for _, widget in ipairs(row) do
  426. local hspan, vspan = scaledSpan(widget)
  427. local y = y_row
  428. if vertical_alignment == 'top' then
  429. y = y - vspan / 2
  430. elseif vertical_alignment == 'bottom' then
  431. y = y - max_height + vspan / 2
  432. else
  433. y = y - max_height / 2
  434. end
  435. local scale = widget.is_panel and select(4, widget.pose:unpack()) or 1
  436. widget.pose = Mat4(x + hspan / 2, y, 0):scale(scale)
  437. x = x + hspan + margin
  438. end
  439. y_row = y_row - max_height - margin
  440. end
  441. -- include panel border in the span
  442. if self.frame == 'backpanel' then
  443. self.span[1] = self.span[1] + 0.5
  444. self.span[2] = self.span[2] + 0.5
  445. end
  446. -- calculate offset from the panel's pose to align to edge or corner of the panel
  447. self.align_offset:set(0, 0)
  448. if horizontal_alignment == 'left' then
  449. self.align_offset:add(self.span[1] / 2, 0, 0)
  450. elseif horizontal_alignment == 'right' then
  451. self.align_offset:add(-self.span[1] / 2, 0, 0)
  452. end
  453. if vertical_alignment == 'top' then
  454. self.align_offset:add(0, -self.span[2] / 2, 0)
  455. elseif vertical_alignment == "bottom" then
  456. self.align_offset:add(0, self.span[2] / 2, 0)
  457. end
  458. end
  459. function panel:updateWidgets(dt, pointers)
  460. if not self.visible then return end
  461. local z_front, z_back = 1.5, -0.3 -- z boundaries of widget AABB
  462. local panel_pose_inv = self:getWorldPose():invert()
  463. for _, widget in ipairs(self.widgets) do
  464. local closest_pos
  465. local closest_name
  466. if widget.interactive then
  467. closest_pos = vec3(math.huge)
  468. for _, pointer in ipairs(pointers) do -- process each pointer
  469. local pos = vec3()
  470. local is_hovered = false
  471. -- reproject pointer onto panel coordinate system and check widget's AABB
  472. local pos_panel = panel_pose_inv:mul(vec3(pointer[2]))
  473. pos = mat4(widget.pose):invert():mul(pos_panel) -- in panel's coordinate system
  474. is_hovered = pos.x > -widget.span[1] / 2 and pos.x < widget.span[1] / 2 and
  475. pos.y > -widget.span[2] / 2 and pos.y < widget.span[2] / 2 and
  476. pos.z < z_front and pos.z > z_back
  477. if is_hovered and math.abs(pos.z) < math.abs(closest_pos.z) then
  478. closest_pos:set(pos)
  479. closest_name = pointer[1]
  480. end
  481. end
  482. end
  483. widget:update(dt, closest_pos, closest_name)
  484. end
  485. end
  486. function panel:getHeadsetPointers(pointers)
  487. for _, hand in ipairs(lovr.headset.getHands()) do
  488. local skeleton = lovr.headset.getSkeleton(hand)
  489. if skeleton then
  490. table.insert(pointers, {hand, vec3(unpack(skeleton[11]))})
  491. else
  492. table.insert(pointers, {hand, vec3(lovr.headset.getPosition(hand .. '/point'))})
  493. end
  494. end
  495. end
  496. function panel:getMousePointer(pointers, click_offset)
  497. -- flatten pose with parent poses
  498. local pose = self:getWorldPose()
  499. local scale = select(4, pose:unpack())
  500. -- overwrite hand/left in desktop VR sim, or make a new pointer for 3d desktop
  501. local mouse_pointer = pointers[1] or {'mouse', vec3()}
  502. -- make a ray in 3D space extending from underneath the mouse cursor to -Z
  503. local mx, my
  504. if lovr.system.isMouseGrabbed() then
  505. mx, my = lovr.system.getWindowDimensions()
  506. mx, my = mx / 2, my / 2
  507. else
  508. mx, my = lovr.system.getMousePosition()
  509. end
  510. local ray_origin = vec3(self.world_from_screen:mul(mx, my, 1))
  511. local ray_target = vec3(self.world_from_screen:mul(mx, my, 0.001))
  512. local ray_direction = (ray_target - ray_origin):normalize()
  513. -- intersect the ray onto panel plane and see if it lands within panel
  514. local plane_direction = quat(pose):direction()
  515. local dot = ray_direction:dot(plane_direction)
  516. if math.abs(dot) > 1e-5 then
  517. local plane_pos = vec3(pose)
  518. local ray_length = (plane_pos - ray_origin):dot(plane_direction) / dot
  519. local hit_spot = ray_origin + ray_direction * ray_length
  520. if click_offset then
  521. if lovr.system.isMouseDown(2) then
  522. mouse_pointer[2]:set(hit_spot)
  523. else -- back off the mouse pointer away from panel to emulate the hovering
  524. mouse_pointer[2]:set(hit_spot + plane_direction * -(0.25 * scale))
  525. end
  526. else
  527. mouse_pointer[2]:set(hit_spot)
  528. end
  529. end
  530. pointers[1] = mouse_pointer
  531. end
  532. function panel:getPointers(click_offset)
  533. local pointers = {}
  534. if lovr.headset then
  535. self:getHeadsetPointers(pointers)
  536. end
  537. if m.mouse_available then
  538. self:getMousePointer(pointers, click_offset)
  539. end
  540. return pointers
  541. end
  542. function panel:getScreenToWorldTransform(pass)
  543. local w, h = pass:getDimensions()
  544. local clip_from_screen = mat4(-1, -1, 0):scale(2 / w, 2 / h, 1)
  545. local view_pose = mat4(pass:getViewPose(1))
  546. local view_proj = pass:getProjection(1, mat4())
  547. -- m.is_orthographic = view_proj[16] == 1
  548. local world_from_screen = view_pose:mul(view_proj:invert()):mul(clip_from_screen)
  549. self.world_from_screen:set(world_from_screen)
  550. end
  551. function panel:getWorldPose()
  552. -- for nested panels, collect all transforms up to parentless root
  553. local stacked_pose = mat4()
  554. local parent = self.parent
  555. local child = self
  556. while parent do
  557. stacked_pose = child.pose * stacked_pose
  558. child = parent
  559. parent = parent.parent
  560. end
  561. -- for root apply both alignment translation and its world pose
  562. stacked_pose = mat4(child.pose):translate(child.align_offset) * stacked_pose
  563. return stacked_pose
  564. end
  565. function panel:update(dt)
  566. if not self.visible then return end
  567. local pointers = self:getPointers(true)
  568. -- TODO: skip update if outside the panel's AABB
  569. self:updateWidgets(dt, pointers)
  570. end
  571. function panel:draw(pass, draw_pointers)
  572. if not self.visible then return end
  573. if m.mouse_available then
  574. self:getScreenToWorldTransform(pass)
  575. end
  576. pass:push()
  577. if self.parent then
  578. pass:transform(0, 0, Q)
  579. else
  580. pass:transform(self.pose)
  581. pass:transform(vec3(self.align_offset))
  582. end
  583. pass:setColor(0.820, 0.816, 0.808)
  584. if self.frame == 'backpanel' then
  585. pass:setColor(self.palette.panel)
  586. pass:roundrect(0, 0, -Q / 2,
  587. self.span[1], self.span[2], Q ,
  588. 0, 0,1,0, 0.4)
  589. end
  590. pass:setFont(m.font)
  591. for _, w in ipairs(self.widgets) do
  592. pass:push()
  593. pass:transform(w.pose)
  594. --[[ widget frames for debugging
  595. pass:setColor(1,0,0)
  596. pass:box(0, 0, 0.2, w.span[1], w.span[2], 0.01, 0, 0,1,0, 'line')
  597. pass:text(w.widget_type or 'FOO', 0, w.span[2] / 2 - 0.2, 0.2, 0.3)
  598. pass:setColor(1,1,1)
  599. --]]
  600. w:draw(pass)
  601. pass:pop()
  602. end
  603. pass:pop()
  604. if draw_pointers then
  605. local pointers = pointers or self:getPointers(false)
  606. pass:setColor(0x404040)
  607. local radius = 0.005
  608. for _, pointer in ipairs(pointers) do
  609. pass:sphere(mat4(pointer[2]):scale(radius), m.segments, m.segments)
  610. end
  611. end
  612. end
  613. function panel:setVisible(is_visible)
  614. if self.visible and not is_visible then
  615. -- complete any ongoing interactions (hovered pointers)
  616. local dt = lovr.timer.getDelta()
  617. for _, widget in ipairs(self.widgets) do
  618. widget:update(dt, vec3(math.huge), nil)
  619. end
  620. end
  621. self.visible = is_visible
  622. end
  623. function panel:appendWidget(widget)
  624. table.insert(self.widgets, widget)
  625. table.insert(self.rows[#self.rows], widget)
  626. end
  627. -- creates panel methods for constructing widgets with light OOP based on metatables
  628. function m.initWidgetType(widget_name, widget_proto)
  629. widget_proto.__index = widget_proto
  630. -- define constructor, for example panel:button{text = 'click'} adds new button to the panel
  631. panel[widget_name] = function(self, options)
  632. options = options or {}
  633. setmetatable(options, widget_proto.defaults)
  634. widget_proto.defaults.__index = widget_proto.defaults
  635. local widget = setmetatable({}, widget_proto)
  636. if type(options.span) == 'number' then
  637. widget.span = {options.span, 1}
  638. elseif type(options.span) == 'table' and #options.span == 2 then
  639. widget.span = {options.span[1], options.span[2]}
  640. elseif not options.span then
  641. widget.span = {1, 1}
  642. else
  643. assert(false, "unsupported widget span value")
  644. end
  645. widget.widget_type = widget_name
  646. widget.parent = self
  647. self:appendWidget(widget)
  648. widget:init(options)
  649. return widget
  650. end
  651. end
  652. local function initAllWidgets()
  653. for _, widget_name in ipairs(m.widget_types) do
  654. local widget_proto = m[widget_name]
  655. m.initWidgetType(widget_name, widget_proto)
  656. end
  657. end
  658. initAllWidgets()
  659. -- CHUI HELPERS ---------------------------------------------------------------
  660. function m.setFont(font) -- accepts path to file or loaded font instance
  661. if type(font) == 'string' then -- path to font file
  662. local ok, res = pcall(lovr.graphics.newFont, font, 32, 4)
  663. if ok then
  664. m.font = res
  665. else
  666. print('could not load \'' .. font .. '\', defaulting to built-in Varela Round')
  667. m.font = lovr.graphics.getDefaultFont()
  668. end
  669. elseif tostring(font):match('Font') then -- a font instance used as-is
  670. m.font = font
  671. else
  672. m.font = lovr.graphics.getDefaultFont()
  673. end
  674. end
  675. -- convenience functions for multiple panels, user can also just call :update & :draw on the panel
  676. function m.update(dt) -- neccessary for UI interactions
  677. for _, pnl in ipairs(m.panels) do
  678. if not pnl.parent then
  679. pnl:update(dt)
  680. end
  681. end
  682. end
  683. function m.draw(pass, draw_pointers)
  684. for _, pnl in ipairs(m.panels) do
  685. if not pnl.parent then
  686. pnl:draw(pass, draw_pointers)
  687. end
  688. end
  689. end
  690. function m.reset() -- forget the collected panels
  691. m.panels = {}
  692. end
  693. return m