-- Copyright (c) 2021 Maksim Tuprikov . This code is licensed under MIT license --- Druid helper module for gui layouts -- @module Helper -- @alias druid.helper local const = require("druid.const") local M = {} --- Text node or icon node can be nil 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_scale(text_node).x return text_metrics.width * text_scale end return 0 end local function get_icon_width(icon_node) if icon_node then local icon_scale_x = gui.get_scale(icon_node).x return gui.get_size(icon_node).x * icon_scale_x -- icon width end return 0 end local function get_width(node) return gui.get_text(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) -- @function helper.centrate_text_with_icon -- @tparam[opt] text text_node Gui text node -- @tparam[opt] box icon_node Gui box node -- @tparam number margin Offset between nodes function M.centrate_text_with_icon(text_node, icon_node, margin) 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) -- @function helper.centrate_icon_with_text -- @tparam[opt] box icon_node Gui box node -- @tparam[opt] text text_node Gui text node -- @tparam[opt=0] number margin Offset between nodes function M.centrate_icon_with_text(icon_node, text_node, margin) M.centrate_nodes(margin, icon_node, text_node) end --- Center several nodes nodes. -- Nodes will be center around 0 x position -- @function helper.centrate_nodes -- @tparam[opt=0] number margin Offset between nodes -- @tparam[opt] Node ... Any count of gui Node 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] local pos = gui.get_position(node) pos_x = pos_x + node_width/2 -- made offset for single item local pivot_offset = M.get_pivot_offset(gui.get_pivot(node)) pos.x = pos_x - width/2 + pivot_offset.x * node_width -- centrate node gui.set_position(node, pos) pos_x = pos_x + node_widths[i]/2 + margin -- add second part of offset end end 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 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 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 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 function M.distance(x1, y1, x2, y2) return math.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2) end function M.sign(val) if val == 0 then return 0 end return (val < 0) and -1 or 1 end function M.round(num, numDecimalPlaces) local mult = 10^(numDecimalPlaces or 0) return math.floor(num * mult + 0.5) / mult end function M.lerp(a, b, t) return a + (b - a) * t end function M.contains(t, value) for i = 1, #t do if t[i] == value then return i end end return false end 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 --- Get text metric from gui node. Replacement of previous gui.get_text_metrics_from_node function -- @tparam Node text_node -- @treturn table {width, height, max_ascent, max_descent} function M.get_text_metrics_from_node(text_node) local font_name = gui.get_font(text_node) local font = gui.get_font_resource(font_name) return resource.get_text_metrics(font, gui.get_text(text_node), { width = gui.get_size(text_node).x, leading = gui.get_leading(text_node), tracking = gui.get_tracking(text_node), line_break = gui.get_line_break(text_node), }) end --- Check if node is enabled in gui hierarchy. -- Return false, if node or any his parent is disabled -- @function helper.is_enabled -- @tparam node node Gui node -- @treturn bool Is enabled in hierarchy function M.is_enabled(node) local is_enabled = gui.is_enabled(node) local parent = gui.get_parent(node) while parent and is_enabled do is_enabled = is_enabled and gui.is_enabled(parent) parent = gui.get_parent(parent) end return is_enabled end --- Get cumulative parent's node scale -- @function helper.get_scene_scale -- @tparam node node Gui node -- @tparam bool include_passed_node_scale True if add current node scale to result -- @treturn 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 node -- @function helper.get_closest_stencil_node -- @tparam node node Gui node -- @treturn node|nil The clipping node 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 -- @function helper.get_pivot_offset -- @tparam gui.pivot pivot The node pivot -- @treturn vector3 Vector offset with [-1..1] values function M.get_pivot_offset(pivot) return const.PIVOTS[pivot] end --- Check if device is mobile (Android or iOS) -- @function helper.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 function M.is_web() return const.CURRENT_SYSTEM_NAME == const.OS.BROWSER end --- Transform table to oneline string -- @tparam table t -- @treturn string function M.table_to_string(t) if not t then return const.EMPTY_STRING 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 The gui node to check -- @tparam vector3 offset The offset to add to result -- @return vector4 Vector with distance to node border: (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 = vmath.mul_per_elem(gui.get_size(node), gui.get_scale(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. Replacement of previous gui.get_text_metrics_from_node function -- @tparam Node text_node -- @treturn table {width, height, max_ascent, max_descent} 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 --- Show deprecated message. Once time per message -- @function helper.deprecated -- @tparam string message The deprecated message 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 extended component function M.extended_component(component_name) print(string.format("[Druid]: The component %s is extended component. You have to register it via druid.register to use it", component_name)) print("[Druid]: Use next code:") print(string.format('local %s = require("druid.extended.%s")', component_name, component_name)) print(string.format('druid.register("%s", %s)', component_name, component_name)) end return M