--- Druid Helper module local const = require("druid.const") local gui_get_node = gui.get_node ---@class druid.system.helper local M = {} local POSITION_X = hash("position.x") local SCALE_X = hash("scale.x") local SIZE_X = hash("size.x") local function get_text_width(text_node) if text_node then local text_metrics = M.get_text_metrics_from_node(text_node) local text_scale = gui.get(text_node, SCALE_X) return text_metrics.width * text_scale end return 0 end local function get_icon_width(icon_node) if icon_node then return gui.get(icon_node, SIZE_X) * gui.get(icon_node, SCALE_X) -- icon width end return 0 end local function is_text_node(node) return gui.get_text(node) ~= nil end --- Text node or icon node can be nil local function get_width(node) return is_text_node(node) and get_text_width(node) or get_icon_width(node) end ---Center two nodes. --Nodes will be center around 0 x position --text_node will be first (at left side) ---@param text_node node|nil Gui text node ---@param icon_node node|nil Gui box node ---@param margin number Offset between nodes ---@local function M.centrate_text_with_icon(text_node, icon_node, margin) return M.centrate_nodes(margin, text_node, icon_node) end ---Center two nodes. --Nodes will be center around 0 x position --icon_node will be first (at left side) ---@param icon_node node|nil Gui box node ---@param text_node node|nil Gui text node ---@param margin number|nil Offset between nodes ---@local function M.centrate_icon_with_text(icon_node, text_node, margin) return M.centrate_nodes(margin, icon_node, text_node) end ---Centerate nodes by x position with margin. --- ---This functions calculate total width of nodes and set position for each node. ---The centrate will be around 0 x position. ---@param margin number|nil Offset between nodes ---@param ... node Nodes to centrate function M.centrate_nodes(margin, ...) margin = margin or 0 local width = 0 local count = select("#", ...) local node_widths = {} -- We need to get total width for i = 1, count do local node = select(i, ...) node_widths[i] = get_width(node) width = width + node_widths[i] end width = width + margin * (count - 1) -- Posing all elements local pos_x = 0 for i = 1, count do local node = select(i, ...) local node_width = node_widths[i] pos_x = pos_x + node_width/2 -- made offset for single item local pivot_offset = M.get_pivot_offset(gui.get_pivot(node)) local new_pos_x = pos_x - width/2 + pivot_offset.x * node_width -- centrate node gui.set(node, POSITION_X, new_pos_x) pos_x = pos_x + node_widths[i]/2 + margin -- add second part of offset end return width end ---@param node_id string|node ---@param template string|nil @Full Path to the template ---@param nodes table|nil @Nodes what created with gui.clone_tree ---@return node function M.get_node(node_id, template, nodes) if type(node_id) ~= "string" then -- Assume it's already node from gui.get_node return node_id end -- If template is set, then add it to the node_id if template and #template > 0 then node_id = template .. "/" .. node_id end -- If nodes is set, then try to find node in it if nodes then return nodes[node_id] end return gui_get_node(node_id) end ---Get current screen stretch multiplier for each side ---@return number stretch_x ---@return number stretch_y function M.get_screen_aspect_koef() local window_x, window_y = window.get_size() local stretch_x = window_x / gui.get_width() local stretch_y = window_y / gui.get_height() return stretch_x / math.min(stretch_x, stretch_y), stretch_y / math.min(stretch_x, stretch_y) end ---Get current GUI scale for each side ---@return number scale_x function M.get_gui_scale() local window_x, window_y = window.get_size() return math.min(window_x / gui.get_width(), window_y / gui.get_height()) end ---Move value from current to target value with step amount ---@param current number Current value ---@param target number Target value ---@param step number Step amount ---@return number New value function M.step(current, target, step) if current < target then return math.min(current + step, target) else return math.max(target, current - step) end end ---Clamp value between min and max ---@param a number Value ---@param min number Min value ---@param max number Max value ---@return number value Clamped value function M.clamp(a, min, max) if min > max then min, max = max, min end if a >= min and a <= max then return a elseif a < min then return min else return max end end ---Calculate distance between two points ---@param x1 number First point x ---@param y1 number First point y ---@param x2 number Second point x ---@param y2 number Second point y ---@return number Distance function M.distance(x1, y1, x2, y2) return math.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2) end ---Return sign of value ---@param val number Value ---@return number sign Sign of value, -1, 0 or 1 function M.sign(val) if val == 0 then return 0 end return (val < 0) and -1 or 1 end ---Round number to specified decimal places ---@param num number Number ---@param num_decimal_places number|nil Decimal places ---@return number value Rounded number function M.round(num, num_decimal_places) local mult = 10^(num_decimal_places or 0) return math.floor(num * mult + 0.5) / mult end ---Lerp between two values ---@param a number First value ---@param b number Second value ---@param t number Lerp amount ---@return number value Lerped value function M.lerp(a, b, t) return a + (b - a) * t end ---Check if value contains in array ---@param array any[] Array to check ---@param value any Value function M.contains(array, value) for index = 1, #array do if array[index] == value then return index end end return nil end ---Make a copy table with all nested tables ---@param orig_table table Original table ---@return table Copy of original table function M.deepcopy(orig_table) local orig_type = type(orig_table) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig_table, nil do copy[M.deepcopy(orig_key)] = M.deepcopy(orig_value) end else -- number, string, boolean, etc copy = orig_table end return copy end ---Add all elements from source array to the target array ---@param target any[] Array to put elements from source ---@param source any[]|nil The source array to get elements from ---@return any[] The target array function M.add_array(target, source) assert(target) if not source then return target end for index = 1, #source do table.insert(target, source[index]) end return target end ---Make a check with gui.pick_node, but with additional node_click_area check. ---@param node node ---@param x number ---@param y number ---@param node_click_area node|nil ---@local function M.pick_node(node, x, y, node_click_area) local is_pick = gui.pick_node(node, x, y) if node_click_area then is_pick = is_pick and gui.pick_node(node_click_area, x, y) end return is_pick end ---Get size of node with scale multiplier ---@param node node GUI node ---@treturn vector3 Scaled size function M.get_scaled_size(node) return vmath.mul_per_elem(gui.get_size(node), gui.get_scale(node)) end ---Get cumulative parent's node scale ---@param node node Gui node ---@param include_passed_node_scale boolean|nil True if add current node scale to result ---@return vector3 The scene node scale function M.get_scene_scale(node, include_passed_node_scale) local scale = include_passed_node_scale and gui.get_scale(node) or vmath.vector3(1) local parent = gui.get_parent(node) while parent do scale = vmath.mul_per_elem(scale, gui.get_scale(parent)) parent = gui.get_parent(parent) end return scale end ---Return closest non inverted clipping parent node for given node ---@param node node GUI node ---@return node|nil stencil_node The closest stencil node or nil function M.get_closest_stencil_node(node) if not node then return nil end local parent = gui.get_parent(node) while parent do local clipping_mode = gui.get_clipping_mode(parent) local is_clipping_normal = not gui.get_clipping_inverted(parent) if is_clipping_normal and clipping_mode == gui.CLIPPING_MODE_STENCIL then return parent end parent = gui.get_parent(parent) end return nil end --- Get node offset for given GUI pivot. -- -- Offset shown in [-0.5 .. 0.5] range, where -0.5 is left or bottom, 0.5 is right or top. -- @function helper.get_pivot_offset -- @tparam number pivot The gui.PIVOT_* constant -- @treturn vector3 Vector offset with [-0.5..0.5] values ---Get pivot offset for given pivot or node ---@param pivot_or_node number|node GUI pivot or node ---@return vector3 offset The pivot offset function M.get_pivot_offset(pivot_or_node) if type(pivot_or_node) == "number" then return const.PIVOTS[pivot_or_node] end return const.PIVOTS[gui.get_pivot(pivot_or_node)] end --- Check if device is native mobile (Android or iOS) -- @function helper.is_mobile -- @treturn boolean Is mobile function M.is_mobile() return const.CURRENT_SYSTEM_NAME == const.OS.IOS or const.CURRENT_SYSTEM_NAME == const.OS.ANDROID end --- Check if device is HTML5 -- @function helper.is_web -- @treturn boolean Is web function M.is_web() return const.CURRENT_SYSTEM_NAME == const.OS.BROWSER end --- Check if device is HTML5 mobile -- @function helper.is_web_mobile -- @treturn boolean Is web mobile function M.is_web_mobile() if html5 then return html5.run("(typeof window.orientation !== 'undefined') || (navigator.userAgent.indexOf('IEMobile') !== -1);") == "true" end return false end --- Check if device is mobile and can support multitouch -- @function helper.is_multitouch_supported -- @treturn boolean Is multitouch supported function M.is_multitouch_supported() return M.is_mobile() or M.is_web_mobile() end --- Simple table to one-line string converter -- @function helper.table_to_string -- @tparam table t -- @treturn string function M.table_to_string(t) if not t then return "" end local result = "{" for key, value in pairs(t) do if #result > 1 then result = result .. "," end result = result .. key .. ": " .. value end return result .. "}" end --- Distance from node position to his borders -- @function helper.get_border -- @tparam node node GUI node -- @tparam vector3|nil offset Offset from node position. Pass current node position to get non relative border values -- @treturn vector4 Vector4 with border values (left, top, right, down) function M.get_border(node, offset) local pivot = gui.get_pivot(node) local pivot_offset = M.get_pivot_offset(pivot) local size = M.get_scaled_size(node) local border = vmath.vector4( -size.x*(0.5 + pivot_offset.x), size.y*(0.5 - pivot_offset.y), size.x*(0.5 - pivot_offset.x), -size.y*(0.5 + pivot_offset.y) ) if offset then border.x = border.x + offset.x border.y = border.y + offset.y border.z = border.z + offset.x border.w = border.w + offset.y end return border end --- Get text metric from GUI node. -- @function helper.get_text_metrics_from_node -- @tparam node text_node -- @treturn GUITextMetrics -- @usage -- type GUITextMetrics = { -- width: number, -- height: number, -- max_ascent: number, -- max_descent: number -- } function M.get_text_metrics_from_node(text_node) local font_resource = gui.get_font_resource(gui.get_font(text_node)) local options = { tracking = gui.get_tracking(text_node), line_break = gui.get_line_break(text_node), } -- Gather other options only if it used in node if options.line_break then options.width = gui.get_size(text_node).x options.leading = gui.get_leading(text_node) end return resource.get_text_metrics(font_resource, gui.get_text(text_node), options) end --- Add value to array with shift policy -- -- Shift policy can be: left, right, no_shift -- @function helper.insert_with_shift -- @tparam table array Array -- @param any Item to insert -- @tparam number|nil index Index to insert. If nil, item will be inserted at the end of array -- @tparam number|nil shift_policy The druid_const.SHIFT.* constant -- @treturn any Inserted item function M.insert_with_shift(array, item, index, shift_policy) shift_policy = shift_policy or const.SHIFT.RIGHT local len = #array index = index or len + 1 if array[index] and shift_policy ~= const.SHIFT.NO_SHIFT then local check_index = index local next_element = array[check_index] while next_element or (check_index >= 1 and check_index <= len) do check_index = check_index + shift_policy local check_element = array[check_index] array[check_index] = next_element next_element = check_element end end array[index] = item return item end --- Remove value from array with shift policy -- -- Shift policy can be: left, right, no_shift -- @function helper.remove_with_shift -- @tparam table array Array -- @tparam number|nil index Index to remove. If nil, item will be removed from the end of array -- @tparam number|nil shift_policy The druid_const.SHIFT.* constant -- @treturn any Removed item function M.remove_with_shift(array, index, shift_policy) shift_policy = shift_policy or const.SHIFT.RIGHT local len = #array index = index or len local item = array[index] array[index] = nil if shift_policy ~= const.SHIFT.NO_SHIFT then local check_index = index + shift_policy local next_element = array[check_index] while next_element or (check_index >= 0 and check_index <= len + 1) do array[check_index - shift_policy] = next_element array[check_index] = nil check_index = check_index + shift_policy next_element = array[check_index] end end return item end --- Show deprecated message. Once time per message -- @function helper.deprecated -- @tparam string message The deprecated message -- @local local _deprecated_messages = {} function M.deprecated(message) if _deprecated_messages[message] then return end print("[Druid]: " .. message) _deprecated_messages[message] = true end --- Show message to require component -- @local function M.require_component_message(component_name, component_type) component_type = component_type or "extended" print(string.format("[Druid]: The component %s is %s component. You have to register it via druid.register to use it", component_name, component_type)) print("[Druid]: Use next code:") print(string.format('local %s = require("druid.%s.%s")', component_name, component_type, component_name)) print(string.format('druid.register("%s", %s)', component_name, component_name)) end return M