Elements.lua 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. local Elements = {_all = {}}
  2. ---@param element Element
  3. function Elements:add(element)
  4. if not element.id then
  5. msg.error('attempt to add element without "id" property')
  6. return
  7. end
  8. if self:has(element.id) then Elements:remove(element.id) end
  9. self._all[#self._all + 1] = element
  10. self[element.id] = element
  11. -- Sort by render order
  12. table.sort(self._all, function(a, b) return a.render_order < b.render_order end)
  13. request_render()
  14. end
  15. function Elements:remove(idOrElement)
  16. if not idOrElement then return end
  17. local id = type(idOrElement) == 'table' and idOrElement.id or idOrElement
  18. local element = Elements[id]
  19. if element then
  20. if not element.destroyed then element:destroy() end
  21. element.enabled = false
  22. self._all = itable_delete_value(self._all, self[id])
  23. self[id] = nil
  24. request_render()
  25. end
  26. end
  27. function Elements:update_proximities()
  28. local curtain_render_order = Elements.curtain.opacity > 0 and Elements.curtain.render_order or 0
  29. local mouse_leave_elements = {}
  30. local mouse_enter_elements = {}
  31. -- Calculates proximities for all elements
  32. for _, element in self:ipairs() do
  33. if element.enabled then
  34. local previous_proximity_raw = element.proximity_raw
  35. -- If curtain is open, we disable all elements set to rendered below it
  36. if not element.ignores_curtain and element.render_order < curtain_render_order then
  37. element:reset_proximity()
  38. else
  39. element:update_proximity()
  40. end
  41. if element.proximity_raw == 0 then
  42. -- Mouse entered element area
  43. if previous_proximity_raw ~= 0 then
  44. mouse_enter_elements[#mouse_enter_elements + 1] = element
  45. end
  46. else
  47. -- Mouse left element area
  48. if previous_proximity_raw == 0 then
  49. mouse_leave_elements[#mouse_leave_elements + 1] = element
  50. end
  51. end
  52. end
  53. end
  54. -- Trigger `mouse_leave` and `mouse_enter` events
  55. for _, element in ipairs(mouse_leave_elements) do element:trigger('mouse_leave') end
  56. for _, element in ipairs(mouse_enter_elements) do element:trigger('mouse_enter') end
  57. end
  58. -- Toggles passed elements' min visibilities between 0 and 1.
  59. ---@param ids string[] IDs of elements to peek.
  60. function Elements:toggle(ids)
  61. local has_invisible = itable_find(ids, function(id)
  62. return Elements[id] and Elements[id].enabled and Elements[id]:get_visibility() ~= 1
  63. end)
  64. self:set_min_visibility(has_invisible and 1 or 0, ids)
  65. -- Reset proximities when toggling off. Has to happen after `set_min_visibility`,
  66. -- as that is using proximity as a tween starting point.
  67. if not has_invisible then
  68. for _, id in ipairs(ids) do
  69. if Elements[id] then Elements[id]:reset_proximity() end
  70. end
  71. end
  72. end
  73. -- Set (animate) elements' min visibilities to passed value.
  74. ---@param visibility number 0-1 floating point.
  75. ---@param ids string[] IDs of elements to peek.
  76. function Elements:set_min_visibility(visibility, ids)
  77. for _, id in ipairs(ids) do
  78. local element = Elements[id]
  79. if element then
  80. local from = math.max(0, element:get_visibility())
  81. element:tween_property('min_visibility', from, visibility)
  82. end
  83. end
  84. end
  85. -- Flash passed elements.
  86. ---@param ids string[] IDs of elements to peek.
  87. function Elements:flash(ids)
  88. local elements = itable_filter(self._all, function(element) return itable_has(ids, element.id) end)
  89. for _, element in ipairs(elements) do element:flash() end
  90. -- Special case for 'progress' since it's a state of timeline, not an element
  91. if itable_has(ids, 'progress') and not itable_has(ids, 'timeline') then
  92. Elements:maybe('timeline', 'flash_progress')
  93. end
  94. end
  95. ---@param name string Event name.
  96. function Elements:trigger(name, ...)
  97. for _, element in self:ipairs() do element:trigger(name, ...) end
  98. end
  99. -- Trigger two events, `name` and `global_name`, depending on element-cursor proximity.
  100. -- Disabled elements don't receive these events.
  101. ---@param name string Event name.
  102. function Elements:proximity_trigger(name, ...)
  103. for i = #self._all, 1, -1 do
  104. local element = self._all[i]
  105. if element.enabled then
  106. if element.proximity_raw == 0 then
  107. if element:trigger(name, ...) == 'stop_propagation' then break end
  108. end
  109. if element:trigger('global_' .. name, ...) == 'stop_propagation' then break end
  110. end
  111. end
  112. end
  113. -- Returns a property of an element with a passed `id` if it exists, with an optional fallback.
  114. ---@param id string
  115. ---@param prop string
  116. ---@param fallback any
  117. function Elements:v(id, prop, fallback)
  118. if self[id] and self[id].enabled and self[id][prop] ~= nil then return self[id][prop] end
  119. return fallback
  120. end
  121. -- Calls a method on an element with passed `id` if it exists.
  122. ---@param id string
  123. ---@param method string
  124. function Elements:maybe(id, method, ...)
  125. if self[id] then return self[id]:maybe(method, ...) end
  126. end
  127. function Elements:has(id) return self[id] ~= nil end
  128. function Elements:ipairs() return ipairs(self._all) end
  129. return Elements