local const = require("druid.const") local helper = require("druid.helper") local component = require("druid.component") local event = require("event.event") ---Druid component to manage a list of data with a scrollable view, used to manage huge list data and render only visible elements. --- ---### Setup ---Create data list component with druid: `data_list = druid:new_data_list(scroll, grid, create_function)` --- ---### Notes ---- Data List uses a scroll component for scrolling and a grid component for layout ---- Data List only renders visible elements for better performance ---- Data List supports caching of elements for better performance ---- Data List supports adding, removing and updating elements ---- Data List supports scrolling to specific elements ---- Data List supports custom element creation and cleanup ---@class druid.data_list: druid.component ---@field scroll druid.scroll The scroll instance for Data List component ---@field grid druid.grid The StaticGrid or DynamicGrid instance for Data List component ---@field on_scroll_progress_change event fun(self: druid.data_list, progress: number) The event triggered when the scroll progress changes ---@field on_element_add event fun(self: druid.data_list, index: number, node: node, instance: druid.component, data: table) The event triggered when a new element is added ---@field on_element_remove event fun(self: druid.data_list, index: number, node: node, instance: druid.component, data: table) The event triggered when an element is removed ---@field top_index number The top index of the visible elements ---@field last_index number The last index of the visible elements ---@field scroll_progress number The scroll progress ---@field private _create_function function The create function callback(self, data, index, data_list). Function should return (node, [component]) ---@field private _is_use_cache boolean Use cache version of DataList. Requires make setup of components in on_element_add callback and clean in on_element_remove ---@field private _cache table The cache table ---@field private _data table The data table ---@field private _data_visual table The data visual table local M = component.create("data_list") ---The DataList constructor ---@param scroll druid.scroll The Scroll instance for Data List component ---@param grid druid.grid The StaticGrid instance for Data List component ---@param create_function function The create function callback(self, data, index, data_list). Function should return (node, [component]) function M:init(scroll, grid, create_function) self.scroll = scroll self.grid = grid if self.grid.style then self.grid.style.IS_DYNAMIC_NODE_POSES = false end -- Current visual elements indexes self.top_index = 1 self.last_index = 1 self.scroll_progress = 0 self._create_function = create_function self._is_use_cache = false self._cache = {} self._data = {} self._data_visual = {} self.scroll.on_scroll:subscribe(self._refresh, self) self.on_scroll_progress_change = event.create() self.on_element_add = event.create() self.on_element_remove = event.create() end ---@private function M:on_remove() self:clear() self.scroll.on_scroll:unsubscribe(self._refresh, self) end ---Set use cache version of DataList. Requires make setup of components in on_element_add callback and clean in on_element_remove ---@param is_use_cache boolean Use cache version of DataList ---@return druid.data_list self Current DataList instance function M:set_use_cache(is_use_cache) self._is_use_cache = is_use_cache return self end ---Set new data set for DataList component ---@param data table The new data array ---@return druid.data_list self Current DataList instance function M:set_data(data) self._data = data or {} self:_refresh() return self end ---Return current data from DataList component ---@return table data The current data array function M:get_data() return self._data end ---Add element to DataList ---@param data table The data to add ---@param index number|nil The index to add the data at ---@param shift_policy number|nil The constant from const.SHIFT.* ---@return druid.data_list self Current DataList instance function M:add(data, index, shift_policy) index = index or #self._data + 1 shift_policy = shift_policy or const.SHIFT.RIGHT helper.insert_with_shift(self._data, data, index, shift_policy) self:_refresh() return self end ---Remove element from DataList ---@param index number|nil The index to remove the data at ---@param shift_policy number|nil The constant from const.SHIFT.* ---@return druid.data_list self Current DataList instance function M:remove(index, shift_policy) helper.remove_with_shift(self._data, index, shift_policy) self:_refresh() return self end ---Remove element from DataList by data value ---@param data table The data to remove ---@param shift_policy number|nil The constant from const.SHIFT.* ---@return druid.data_list self Current DataList instance function M:remove_by_data(data, shift_policy) local index = helper.contains(self._data, data) if index then helper.remove_with_shift(self._data, index, shift_policy) self:_refresh() end return self end ---Clear the DataList and refresh visuals ---@return druid.data_list self Current DataList instance function M:clear() self._data = {} self:_refresh() return self end ---Return index for data value ---@param data table function M:get_index(data) for index, value in pairs(self._data) do if value == data then return index end end return nil end ---Return all currently created nodes in DataList ---@return node[] List of created nodes function M:get_created_nodes() local nodes = {} for index, data in pairs(self._data_visual) do nodes[index] = data.node end return nodes end ---Return all currently created components in DataList ---@return druid.component[] components List of created components function M:get_created_components() local components = {} for index, data in pairs(self._data_visual) do components[index] = data.component end return components end ---Instant scroll to element with passed index ---@param index number The index to scroll to function M:scroll_to_index(index) local pos = self.grid:get_pos(index) self.scroll:scroll_to(pos) end ---Add element at passed index using cache or create new ---@param index number The index to add the element at ---@private function M:_add_at(index) if self._data_visual[index] then self:_remove_at(index) end local data = self._data[index] local node, instance -- Use cache if available and is_use_cache is set if #self._cache > 0 and self._is_use_cache then local cached = table.remove(self._cache) node = cached.node instance = cached.component gui.set_enabled(node, true) else -- Create a new element if no cache or refresh function is not set node, instance = self._create_function(self:get_context(), data, index, self) end self._data_visual[index] = { data = data, node = node, component = instance, } self.grid:add(node, index, const.SHIFT.NO_SHIFT) self.on_element_add:trigger(self:get_context(), index, node, instance, data) end ---Remove element from passed index and add it to cache if applicable ---@param index number The index to remove the element at ---@private function M:_remove_at(index) self.grid:remove(index, const.SHIFT.NO_SHIFT) local visual_data = self._data_visual[index] local node = visual_data.node local instance = visual_data.component local data = visual_data.data self.on_element_remove:trigger(self:get_context(), index, node, instance, data) if self._is_use_cache then -- Disable the node and add it to the cache instead of deleting it gui.set_enabled(node, false) table.insert(self._cache, visual_data) -- Cache the removed element else -- If no refresh function, delete the node and component as usual gui.delete_node(node) if instance then instance._meta.druid:remove(instance) end end self._data_visual[index] = nil end ---Refresh all elements in DataList ---@private function M:_refresh() self.scroll:set_size(self.grid:get_size_for(#self._data)) local start_pos = -self.scroll.position --[[@as vector3]] local start_index = self.grid:get_index(start_pos) start_index = math.max(1, start_index) local offset_x = self.scroll.view_size.x local offset_y = self.scroll.view_size.y local end_pos = vmath.vector3(start_pos.x + offset_x, start_pos.y - offset_y, 0) local max_offset_x = (self.grid.in_row - 1) * self.grid.node_size.x end_pos.x = math.min(end_pos.x, start_pos.x + max_offset_x) if #self._data <= self.grid.in_row then end_pos.y = start_pos.y end local end_index = self.grid:get_index(end_pos) end_index = math.min(#self._data, end_index) self.top_index = start_index self.last_index = end_index -- Clear from non range elements for index, data in pairs(self._data_visual) do if index < start_index or index > end_index then self:_remove_at(index) elseif self._data[index] ~= data.data then -- TODO We want to find currently created data instances and move them to new positions -- Now it will re-create them self:_remove_at(index) end end -- Add new elements for index = start_index, end_index do if not self._data_visual[index] and self._data[index] then self:_add_at(index) end end end return M