Merge branch 'develop' into 43-infinity-scroll

# Conflicts:
#	druid/base/grid.lua
#	druid/const.lua
#	druid/system/druid_instance.lua
This commit is contained in:
Insality
2020-11-03 23:25:02 +03:00
101 changed files with 15213 additions and 8762 deletions

View File

@@ -0,0 +1,89 @@
--- Druid checkbox component
-- @module Checkbox
-- @within BaseComponent
-- @alias druid.checkbox
--- On change state callback(self, state)
-- @tfield druid_event on_change_state
--- Visual node
-- @tfield node node
--- Button trigger node
-- @tfield[opt=node] node click_node
--- Button component from click_node
-- @tfield Button button
local const = require("druid.const")
local Event = require("druid.event")
local component = require("druid.component")
local Checkbox = component.create("checkbox", { const.ON_LAYOUT_CHANGE })
local function on_click(self)
self:set_state(not self.state)
end
--- Component style params.
-- You can override this component styles params in druid styles table
-- or create your own style
-- @table style
-- @tfield function on_change_state (self, node, state)
function Checkbox.on_style_change(self, style)
self.style = {}
self.style.on_change_state = style.on_change_state or function(_, node, state)
gui.set_enabled(node, state)
end
end
--- Component init function
-- @tparam Checkbox self
-- @tparam node node Gui node
-- @tparam function callback Checkbox callback
-- @tparam[opt=node] node click_node Trigger node, by default equals to node
function Checkbox.init(self, node, callback, click_node)
self.druid = self:get_druid()
self.node = self:get_node(node)
self.click_node = self:get_node(click_node)
self.button = self.druid:new_button(self.click_node or self.node, on_click)
self:set_state(false, true)
self.on_change_state = Event(callback)
end
function Checkbox.on_layout_change(self)
self:set_state(self.state, true)
end
--- Set checkbox state
-- @tparam Checkbox self
-- @tparam bool state Checkbox state
-- @tparam bool is_silent Don't trigger on_change_state if true
function Checkbox.set_state(self, state, is_silent)
self.state = state
self.style.on_change_state(self, self.node, state)
if not is_silent then
self.on_change_state:trigger(self:get_context(), state)
end
end
--- Return checkbox state
-- @tparam Checkbox self
-- @treturn bool Checkbox state
function Checkbox.get_state(self)
return self.state
end
return Checkbox

View File

@@ -0,0 +1,67 @@
--- Checkbox group module
-- @module CheckboxGroup
-- @within BaseComponent
-- @alias druid.checkbox_group
--- On any checkbox click callback(self, index)
-- @tfield druid_event on_checkbox_click
--- Array of checkbox components
-- @tfield table checkboxes
local Event = require("druid.event")
local component = require("druid.component")
local CheckboxGroup = component.create("checkbox_group")
--- Component init function
-- @tparam CheckboxGroup self
-- @tparam node[] nodes Array of gui node
-- @tparam function callback Checkbox callback
-- @tparam[opt=node] node[] click_nodes Array of trigger nodes, by default equals to nodes
function CheckboxGroup.init(self, nodes, callback, click_nodes)
self.druid = self:get_druid()
self.checkboxes = {}
self.on_checkbox_click = Event(callback)
for i = 1, #nodes do
local click_node = click_nodes and click_nodes[i] or nil
local checkbox = self.druid:new_checkbox(nodes[i], function()
self.on_checkbox_click:trigger(self:get_context(), i)
end, click_node)
table.insert(self.checkboxes, checkbox)
end
end
--- Set checkbox group state
-- @tparam CheckboxGroup self
-- @tparam bool[] indexes Array of checkbox state
function CheckboxGroup.set_state(self, indexes)
for i = 1, #indexes do
if self.checkboxes[i] then
self.checkboxes[i]:set_state(indexes[i], true)
end
end
end
--- Return checkbox group state
-- @tparam CheckboxGroup self
-- @treturn bool[] Array if checkboxes state
function CheckboxGroup.get_state(self)
local result = {}
for i = 1, #self.checkboxes do
table.insert(result, self.checkboxes[i]:get_state())
end
return result
end
return CheckboxGroup

View File

@@ -0,0 +1,67 @@
--- Druid component template
-- @module druid.component
-- @local
local const = require("druid.const")
local component = require("druid.component")
local Component = component.create("my_component_name", { const.ON_UPDATE })
-- Component constructor
function Component:init(...)
end
-- Call only if exist interest: const.ON_UPDATE
function Component:update(dt)
end
-- Call only if exist interest: const.ON_INPUT or const.ON_INPUT_HIGH
function Component:on_input(action_id, action)
return false
end
-- Call on component creation and on component:set_style() function
function Component:on_style_change(style)
end
-- Call only if exist interest: const.ON_MESSAGE
function Component:on_message(message_id, message, sender)
end
-- Call only if component with ON_LANGUAGE_CHANGE interest
function Component:on_language_change()
end
-- Call only if component with ON_LAYOUT_CHANGE interest
function Component:on_layout_change()
end
-- Call, if input was capturing before this component
-- Example: scroll is start scrolling, so you need unhover button
function Component:on_input_interrupt()
end
-- Call, if game lost focus. Need ON_FOCUS_LOST intereset
function Component:on_focus_lost()
end
-- Call, if game gained focus. Need ON_FOCUS_GAINED intereset
function Component:on_focus_gained()
end
-- Call on component remove or on druid:final
function Component:on_remove()
end
return Component

View File

@@ -0,0 +1,415 @@
--- Component to handle placing components in row
-- @module DynamicGrid
-- @within BaseComponent
-- @alias druid.dynamic_grid
--- On item add callback(self, node, index)
-- @tfield druid_event on_add_item
--- On item remove callback(self, index)
-- @tfield druid_event on_remove_item
--- On item add or remove callback(self, index)
-- @tfield druid_event on_change_items
--- On grid clear callback(self)
-- @tfield druid_event on_clear
--- On update item positions callback(self)
-- @tfield druid_event on_update_positions
--- Parent gui node
-- @tfield node parent
--- List of all grid nodes
-- @tfield node[] nodes
--- The first index of node in grid
-- @tfield number first_index
--- The last index of node in grid
-- @tfield number last_index
--- Item size
-- @tfield vector3 node_size
--- The size of item content
-- @tfield vector4 border
local const = require("druid.const")
local Event = require("druid.event")
local helper = require("druid.helper")
local component = require("druid.component")
local DynamicGrid = component.create("dynamic_grid", { const.ON_LAYOUT_CHANGE })
local SIDE_VECTORS = {
LEFT = vmath.vector3(-1, 0, 0),
RIGHT = vmath.vector3(1, 0, 0),
TOP = vmath.vector3(0, -1, 0),
BOT = vmath.vector3(0, 1, 0),
}
local AVAILABLE_PIVOTS = {
gui.PIVOT_N,
gui.PIVOT_S,
gui.PIVOT_W,
gui.PIVOT_E,
}
--- Component init function
-- @tparam DynamicGrid self
-- @tparam node parent The gui node parent, where items will be placed
function DynamicGrid.init(self, parent)
self.parent = self:get_node(parent)
local parent_pivot = gui.get_pivot(self.parent)
self.pivot = helper.get_pivot_offset(parent_pivot)
assert(helper.contains(AVAILABLE_PIVOTS, parent_pivot), const.ERRORS.GRID_DYNAMIC_ANCHOR)
self.side = ((parent_pivot == gui.PIVOT_W or parent_pivot == gui.PIVOT_E)
and const.SIDE.X or const.SIDE.Y)
self.nodes = {}
self.border = vmath.vector4(0) -- Current grid content size
self.on_add_item = Event()
self.on_remove_item = Event()
self.on_change_items = Event()
self.on_clear = Event()
self.on_update_positions = Event()
self._set_position_function = gui.set_position
end
function DynamicGrid.on_layout_change(self)
self:_update(true)
end
--- Return pos for grid node index
-- @tparam DynamicGrid self
-- @tparam number index The grid element index
-- @tparam node node The node to be placed
-- @tparam[opt] number origin_index Index of nearby node
-- @treturn vector3 Node position
function DynamicGrid.get_pos(self, index, node, origin_index)
local origin_node = self.nodes[origin_index]
-- If anchor node is not exist, check around nodes
if not origin_node then
if self.nodes[index + 1] then
origin_index = index + 1
end
if self.nodes[index - 1] then
origin_index = index - 1
end
origin_node = self.nodes[origin_index]
end
if not origin_node then
assert(not self.first_index, "Dynamic Grid can't have gaps between nodes. Error on grid:add")
-- If not origin node, so it should be first element in the grid
local size = self:_get_node_size(node)
local pivot = const.PIVOTS[gui.get_pivot(node)]
return vmath.vector3(
size.x * pivot.x - size.x * self.pivot.x,
size.y * pivot.y - size.y * self.pivot.y,
0)
end
if origin_node then
-- Other nodes spawn from other side of the origin node
local is_forward = origin_index < index
local delta = is_forward and 1 or -1
return self:_get_next_node_pos(index - delta, node, self:_get_side_vector(self.side, is_forward))
end
end
--- Add new node to the grid
-- @tparam DynamicGrid self
-- @tparam node node Gui node
-- @tparam[opt] number index The node position. By default add as last node
-- @tparam[opt=false] bool is_shift_left If true, shift all nodes to the left, otherwise shift nodes to the right
function DynamicGrid.add(self, node, index, is_shift_left)
local delta = is_shift_left and -1 or 1
-- By default add node at end
index = index or ((self.last_index or 0) + 1)
-- If node exist at index place, shifting them
local is_shift = self.nodes[index]
if is_shift then
-- We need to iterate from index to start or end grid, depends of shift side
local start_index = is_shift_left and self.first_index or self.last_index
for i = start_index, index, -delta do
self.nodes[i + delta] = self.nodes[i]
end
end
self:_add_node(node, index, index - delta)
-- After shifting we should recalc node poses
if is_shift then
-- We need to iterate from placed node to start or end grid, depends of shift side
local target_index = is_shift_left and self.first_index or self.last_index
for i = index + delta, target_index + delta, delta do
local move_node = self.nodes[i]
move_node.pos = self:get_pos(i, move_node.node, i - delta)
end
end
-- Sync grid data
self:_update()
self.on_add_item:trigger(self:get_context(), node, index)
self.on_change_items:trigger(self:get_context(), index)
end
--- Remove the item from the grid. Note that gui node will be not deleted
-- @tparam DynamicGrid self
-- @tparam number index The grid node index to remove
-- @tparam[opt=false] bool is_shift_left If true, shift all nodes to the left, otherwise shift nodes to the right
-- @treturn Node The deleted gui node from grid
function DynamicGrid.remove(self, index, is_shift_left)
local delta = is_shift_left and -1 or 1
assert(self.nodes[index], "No grid item at given index " .. index)
-- Just set nil for delete node data
local removed_node = self.nodes[index].node
self.nodes[index] = nil
-- After delete node, we should shift nodes and recalc their poses, depends from is_shift_left
local target_index = is_shift_left and self.first_index or self.last_index
for i = index, target_index, delta do
self.nodes[i] = self.nodes[i + delta]
if self.nodes[i] then
self.nodes[i].pos = self:get_pos(i, self.nodes[i].node, i - delta)
end
end
-- Sync grid data
self:_update()
self.on_remove_item:trigger(self:get_context(), index)
self.on_change_items:trigger(self:get_context(), index)
return removed_node
end
--- Return grid content size
-- @tparam DynamicGrid self
-- @tparam vector3 border
-- @treturn vector3 The grid content size
function DynamicGrid.get_size(self, border)
border = border or self.border
return vmath.vector3(
border.z - border.x,
border.y - border.w,
0)
end
--- Return grid index by node
-- @tparam DynamicGrid self
-- @tparam node node The gui node in the grid
-- @treturn number The node index
function DynamicGrid.get_index_by_node(self, node)
for index, node_info in pairs(self.nodes) do
if node == node_info.node then
return index
end
end
return nil
end
--- Return array of all node positions
-- @tparam DynamicGrid self
-- @treturn vector3[] All grid node positions
function DynamicGrid.get_all_pos(self)
local result = {}
for i, node in pairs(self.nodes) do
table.insert(result, gui.get_position(node))
end
return result
end
--- Change set position function for grid nodes. It will call on
-- update poses on grid elements. Default: gui.set_position
-- @tparam DynamicGrid self
-- @tparam function callback Function on node set position
-- @treturn druid.dynamic_grid Current grid instance
function DynamicGrid.set_position_function(self, callback)
self._set_position_function = callback or gui.set_position
return self
end
--- Clear grid nodes array. GUI nodes will be not deleted!
-- If you want to delete GUI nodes, use dynamic_grid.nodes array before grid:clear
-- @tparam DynamicGrid self
-- @treturn druid.dynamic_grid Current grid instance
function DynamicGrid.clear(self)
self.nodes = {}
self:_update()
self.on_clear:trigger(self:get_context())
return self
end
function DynamicGrid._add_node(self, node, index, origin_index)
self.nodes[index] = {
node = node,
pos = self:get_pos(index, node, origin_index),
size = self:_get_node_size(node),
pivot = const.PIVOTS[gui.get_pivot(node)]
}
-- Add new item instantly in new pos
gui.set_parent(node, self.parent)
gui.set_position(node, self.nodes[index].pos + self:_get_zero_offset())
end
--- Update grid inner state
-- @tparam DynamicGrid self
-- @tparam bool is_instant If true, node position update instantly, otherwise with set_position_function callback
-- @local
function DynamicGrid._update(self, is_instant)
self:_update_indexes()
self:_update_borders()
self:_update_pos(is_instant)
end
--- Update first and last indexes of grid nodes
-- @tparam DynamicGrid self
-- @local
function DynamicGrid._update_indexes(self)
self.first_index = nil
self.last_index = nil
for index in pairs(self.nodes) do
self.first_index = self.first_index or index
self.last_index = self.last_index or index
self.first_index = math.min(self.first_index, index)
self.last_index = math.max(self.last_index, index)
end
end
--- Update grid content borders, recalculate min and max values
-- @tparam DynamicGrid self
-- @local
function DynamicGrid._update_borders(self)
if not self.first_index then
self.border = vmath.vector4(0)
return
end
self.border = vmath.vector4(math.huge, -math.huge, -math.huge, math.huge)
for index, node in pairs(self.nodes) do
local pos = node.pos
local size = node.size
local pivot = node.pivot
local left = pos.x - size.x/2 - (size.x * pivot.x)
local right = pos.x + size.x/2 - (size.x * pivot.x)
local top = pos.y + size.y/2 - (size.y * pivot.y)
local bottom = pos.y - size.y/2 - (size.y * pivot.y)
self.border.x = math.min(self.border.x, left)
self.border.y = math.max(self.border.y, top)
self.border.z = math.max(self.border.z, right)
self.border.w = math.min(self.border.w, bottom)
end
end
--- Update grid nodes position
-- @tparam DynamicGrid self
-- @tparam bool is_instant If true, node position update instantly, otherwise with set_position_function callback
-- @local
function DynamicGrid._update_pos(self, is_instant)
local offset = self:_get_zero_offset()
for index, node in pairs(self.nodes) do
if is_instant then
gui.set_position(node.node, node.pos + offset)
else
self._set_position_function(node.node, node.pos + offset)
end
end
self.on_update_positions:trigger(self:get_context())
end
function DynamicGrid._get_next_node_pos(self, origin_node_index, new_node, place_side)
local node = self.nodes[origin_node_index]
local new_node_size = self:_get_node_size(new_node)
local new_pivot = const.PIVOTS[gui.get_pivot(new_node)]
local dist_x = (node.size.x/2 + new_node_size.x/2) * place_side.x
local dist_y = (node.size.y/2 + new_node_size.y/2) * place_side.y
local node_center_x = node.pos.x - node.size.x * node.pivot.x
local node_center_y = node.pos.y - node.size.y * node.pivot.y
return vmath.vector3(
node_center_x + dist_x + new_node_size.x * new_pivot.x,
node_center_y - dist_y + new_node_size.y * new_pivot.y,
0
)
end
function DynamicGrid._get_node_size(self, node)
return vmath.mul_per_elem(gui.get_size(node), gui.get_scale(node))
end
--- Return elements offset for correct posing nodes. Correct posing at
-- parent pivot node (0:0) with adjusting of node sizes and anchoring
-- @tparam DynamicGrid self
-- @treturn vector3 The offset vector
-- @local
function DynamicGrid._get_zero_offset(self)
-- zero offset: center pos - border size * anchor
return vmath.vector3(
-((self.border.x + self.border.z)/2 + (self.border.z - self.border.x) * self.pivot.x),
-((self.border.y + self.border.w)/2 + (self.border.y - self.border.w) * self.pivot.y),
0)
end
--- Return side vector to correct node shifting
function DynamicGrid._get_side_vector(self, side, is_forward)
if side == const.SIDE.X then
return is_forward and SIDE_VECTORS.RIGHT or SIDE_VECTORS.LEFT
end
if side == const.SIDE.Y then
return is_forward and SIDE_VECTORS.BOT or SIDE_VECTORS.TOP
end
end
return DynamicGrid

View File

@@ -0,0 +1,117 @@
--- Manage data for huge dataset in scroll
--- It requires basic druid scroll and druid grid components
local const = require("druid.const")
local component = require("druid.component")
local M = component.create("infinity_list", { const.ON_UPDATE })
function M:init(data_list, scroll, grid, create_function)
self.view_size = gui.get_size(scroll.view_node)
self.prefab_size = grid.node_size
self.druid = self:get_druid()
self.scroll = scroll
self.grid = grid
self.data = data_list
self.top_index = 1
self.create_function = create_function
self.nodes = {}
self.components = {}
self.elements_view_count = vmath.vector3(
math.min(math.ceil(self.view_size.x / self.prefab_size.x), self.grid.in_row),
math.ceil(self.view_size.y / self.prefab_size.y),
0)
self:_refresh()
self.scroll.on_scroll:subscribe(function() self._check_elements(self) end)
end
function M:on_remove()
-- TODO: make this work
-- self.scroll.on_scroll:unsubscribe(self._check_elements)
end
function M:update(dt)
if self.scroll.animate then
self:_check_elements()
end
end
function M:set_data(data_list)
self.data = data_list
self:_refresh()
end
function M:_add_at(index)
if self.nodes[index] then
self:_remove_at(index)
end
local node, instance = self.create_function(self.data[index], index)
self.grid:add(node, index)
self.nodes[index] = node
self.components[index] = instance
end
function M:_remove_at(index)
self.grid:remove(index)
local node = self.nodes[index]
gui.delete_node(node)
self.nodes[index] = nil
if self.components[index] then
self.druid:remove(self.components[index])
self.components[index] = nil
end
end
function M:_refresh()
for index, _ in pairs(self.nodes) do
self:_remove_at(index)
end
self:_check_elements()
self:_recalc_scroll_size()
end
function M:_check_elements()
local pos = gui.get_position(self.scroll.content_node)
pos.y = -pos.y
local top_index = self.grid:get_index(pos)
local last_index = top_index + (self.elements_view_count.x * self.elements_view_count.y) + self.grid.in_row - 1
-- Clear outside elements
for index, _ in pairs(self.nodes) do
if index < top_index or index > last_index then
self:_remove_at(index)
end
end
-- Spawn current elements
for index = top_index, last_index do
if self.data[index] and not self.nodes[index] then
self:_add_at(index)
end
end
end
function M:_recalc_scroll_size()
local element_size = self.grid:get_size_for_elements_count(#self.data)
self.scroll:set_size(element_size)
end
return M

338
druid/extended/input.lua Normal file
View File

@@ -0,0 +1,338 @@
--- Druid input text component.
-- Carry on user text input
-- @author Part of code from Britzl gooey input component
-- @module Input
-- @within BaseComponent
-- @alias druid.input
--- On input field select callback(self, button_node)
-- @tfield druid_event on_input_select
--- On input field unselect callback(self, button_node)
-- @tfield druid_event on_input_unselect
--- On input field text change callback(self, input_text)
-- @tfield druid_event on_input_text
--- On input field text change to empty string callback(self, input_text)
-- @tfield druid_event on_input_empty
--- On input field text change to max length string callback(self, input_text)
-- @tfield druid_event on_input_full
--- On trying user input with not allowed character callback(self, params, button_instance)
-- @tfield druid_event on_input_wrong
--- Text component
-- @tfield druid.text text
--- Button component
-- @tfield druid.button button
--- Is current input selected now
-- @tfield bool is_selected
--- Is current input is empty now
-- @tfield bool is_empty
--- Max length for input text
-- @tfield[opt] number max_length
--- Pattern matching for user input
-- @tfield[opt] string allowerd_characters
--- Gui keyboard type for input field
-- @tfield number keyboard_type
local Event = require("druid.event")
local const = require("druid.const")
local component = require("druid.component")
local utf8 = require("druid.system.utf8")
local Input = component.create("input", { const.ON_INPUT, const.ON_FOCUS_LOST })
--- Mask text by replacing every character with a mask character
-- @tparam string text
-- @tparam string mask
-- @treturn string Masked text
local function mask_text(text, mask)
mask = mask or "*"
local masked_text = ""
for uchar in utf8.gmatch(text, ".") do
masked_text = masked_text .. mask
end
return masked_text
end
local function select(self)
gui.reset_keyboard()
self.marked_value = ""
if not self.is_selected then
self:increase_input_priority()
self.button:increase_input_priority()
self.previous_value = self.value
self.is_selected = true
gui.show_keyboard(self.keyboard_type, false)
self.on_input_select:trigger(self:get_context())
self.style.on_select(self, self.button.node)
end
end
local function unselect(self)
gui.reset_keyboard()
self.marked_value = ""
if self.is_selected then
self:reset_input_priority()
self.button:reset_input_priority()
self.is_selected = false
gui.hide_keyboard()
self.on_input_unselect:trigger(self:get_context())
self.style.on_unselect(self, self.button.node)
end
end
local function clear_and_select(self)
if self.style.IS_LONGTAP_ERASE then
self:set_text("")
end
select(self)
end
--- Component style params.
-- You can override this component styles params in druid styles table
-- or create your own style
-- @table style
-- @tfield[opt=false] bool IS_LONGTAP_ERASE Is long tap will erase current input data
-- @tfield[opt=*] string MASK_DEFAULT_CHAR Default character mask for password input
-- @tfield function on_select (self, button_node) Callback on input field selecting
-- @tfield function on_unselect (self, button_node) Callback on input field unselecting
-- @tfield function on_input_wrong (self, button_node) Callback on wrong user input
-- @tfield table button_style Custom button style for input node
function Input.on_style_change(self, style)
self.style = {}
self.style.IS_LONGTAP_ERASE = style.IS_LONGTAP_ERASE or false
self.style.MASK_DEFAULT_CHAR = style.MASK_DEFAULT_CHAR or "*"
self.style.on_select = style.on_select or function(_, button_node) end
self.style.on_unselect = style.on_unselect or function(_, button_node) end
self.style.on_input_wrong = style.on_input_wrong or function(_, button_node) end
self.style.button_style = style.button_style or {
LONGTAP_TIME = 0.4,
AUTOHOLD_TRIGGER = 0.8,
DOUBLETAP_TIME = 0.4
}
end
-- @tparam node click_node Button node to enabled input component
-- @tparam node text_node Text node what will be changed on user input
-- @tparam[opt] number keyboard_type Gui keyboard type for input field
function Input.init(self, click_node, text_node, keyboard_type)
self.druid = self:get_druid(self)
self.text = self.druid:new_text(text_node)
self.is_selected = false
self.value = self.text.last_value
self.previous_value = self.text.last_value
self.current_value = self.text.last_value
self.marked_value = ""
self.is_empty = true
self.text_width = 0
self.market_text_width = 0
self.total_width = 0
self.max_length = nil
self.allowed_characters = nil
self.keyboard_type = keyboard_type or gui.KEYBOARD_TYPE_DEFAULT
self.button = self.druid:new_button(click_node, select)
self.button:set_style(self.button_style)
self.button.on_click_outside:subscribe(unselect)
self.button.on_long_click:subscribe(clear_and_select)
self.on_input_select = Event()
self.on_input_unselect = Event()
self.on_input_text = Event()
self.on_input_empty = Event()
self.on_input_full = Event()
self.on_input_wrong = Event()
end
function Input.on_input(self, action_id, action)
if self.is_selected then
local input_text = nil
if action_id == const.ACTION_TEXT then
-- ignore return key
if action.text == "\n" or action.text == "\r" then
return true
end
local hex = string.gsub(action.text,"(.)", function (c)
return string.format("%02X%s",string.byte(c), "")
end)
-- ignore arrow keys
if not string.match(hex, "EF9C8[0-3]") then
if not self.allowed_characters or action.text:match(self.allowed_characters) then
input_text = self.value .. action.text
if self.max_length then
input_text = utf8.sub(input_text, 1, self.max_length)
end
else
self.on_input_wrong:trigger(self:get_context(), action.text)
self.style.on_input_wrong(self, self.button.node)
end
self.marked_value = ""
end
end
if action_id == const.ACTION_MARKED_TEXT then
self.marked_value = action.text or ""
if self.max_length then
self.marked_value = utf8.sub(self.marked_value, 1, self.max_length)
end
end
if action_id == const.ACTION_BACKSPACE and (action.pressed or action.repeated) then
input_text = utf8.sub(self.value, 1, -2)
end
if action_id == const.ACTION_ENTER and action.released then
unselect(self)
return true
end
if action_id == const.ACTION_BACK and action.released then
unselect(self)
return true
end
if action_id == const.ACTION_ESC and action.released then
unselect(self)
return true
end
if input_text or #self.marked_value > 0 then
self:set_text(input_text)
return true
end
end
return self.is_selected
end
function Input.on_focus_lost(self)
unselect(self)
end
function Input.on_input_interrupt(self)
-- unselect(self)
end
--- Set text for input field
-- @tparam Input self
-- @tparam string input_text The string to apply for input field
function Input.set_text(self, input_text)
-- Case when update with marked text
if input_text then
self.value = input_text
end
-- Only update the text if it has changed
local current_value = self.value .. self.marked_value
if current_value ~= self.current_value then
self.current_value = current_value
-- mask text if password field
local masked_value, masked_marked_value
if self.keyboard_type == gui.KEYBOARD_TYPE_PASSWORD then
local mask_char = self.style.MASK_DEFAULT_CHAR or "*"
masked_value = mask_text(self.value, mask_char)
masked_marked_value = mask_text(self.marked_value, mask_char)
end
-- text + marked text
local value = masked_value or self.value
local marked_value = masked_marked_value or self.marked_value
self.is_empty = #value == 0 and #marked_value == 0
local final_text = value .. marked_value
self.text:set_to(final_text)
-- measure it
self.text_width = self.text:get_text_width(value)
self.marked_text_width = self.text:get_text_width(marked_value)
self.total_width = self.text_width + self.marked_text_width
self.on_input_text:trigger(self:get_context(), final_text)
if #final_text == 0 then
self.on_input_empty:trigger(self:get_context(), final_text)
end
if self.max_length and #final_text == self.max_length then
self.on_input_full:trigger(self:get_context(), final_text)
end
end
end
--- Return current input field text
-- @tparam Input self
-- @treturn string The current input field text
function Input.get_text(self)
return self.value .. self.marked_value
end
--- Set maximum length for input field.
-- Pass nil to make input field unliminted (by default)
-- @tparam Input self
-- @tparam number max_length Maximum length for input text field
-- @treturn druid.input Current input instance
function Input.set_max_length(self, max_length)
self.max_length = max_length
return self
end
--- Set allowed charaters for input field.
-- See: https://defold.com/ref/stable/string/
-- ex: [%a%d] for alpha and numeric
-- @tparam Input self
-- @tparam string characters Regulax exp. for validate user input
-- @treturn druid.input Current input instance
function Input.set_allowed_characters(self, characters)
self.allowed_characters = characters
return self
end
--- Reset current input selection and return previous value
-- @tparam Input self
function Input.reset_changes(self)
self:set_text(self.previous_value)
unselect(self)
end
return Input

View File

@@ -0,0 +1,67 @@
--- Component to handle all GUI texts
-- Good working with localization system
-- @module LangText
-- @within BaseComponent
-- @alias druid.lang_text
--- On change text callback
-- @tfield druid_event on_change
--- The text component
-- @tfield Text text
local Event = require("druid.event")
local const = require("druid.const")
local settings = require("druid.system.settings")
local component = require("druid.component")
local LangText = component.create("lang_text", { const.ON_LANGUAGE_CHANGE })
--- Component init function
-- @tparam LangText self
-- @tparam node node The text node
-- @tparam string locale_id Default locale id
-- @tparam bool no_adjust If true, will not correct text size
function LangText.init(self, node, locale_id, no_adjust)
self.druid = self:get_druid()
self.text = self.druid:new_text(node, locale_id, no_adjust)
self.last_locale_args = {}
self.on_change = Event()
self:translate(locale_id)
return self
end
function LangText.on_language_change(self)
if self.last_locale then
self:translate(self.last_locale, unpack(self.last_locale_args))
end
end
--- Setup raw text to lang_text component
-- @tparam LangText self
-- @tparam string text Text for text node
function LangText.set_to(self, text)
self.last_locale = false
self.text:set_to(text)
self.on_change:trigger()
end
--- Translate the text by locale_id
-- @tparam LangText self
-- @tparam string locale_id Locale id
function LangText.translate(self, locale_id, ...)
self.last_locale_args = {...}
self.last_locale = locale_id or self.last_locale
self.text:set_to(settings.get_text(self.last_locale, ...))
end
return LangText

204
druid/extended/progress.lua Normal file
View File

@@ -0,0 +1,204 @@
--- Basic progress bar component.
-- For correct progress bar init it should be in max size from gui
-- @module Progress
-- @within BaseComponent
-- @alias druid.progress
--- On progress bar change callback(self, new_value)
-- @tfield druid_event on_change
--- Progress bar fill node
-- @tfield node node
--- The progress bar direction
-- @tfield string key
--- Current progress bar scale
-- @tfield vector3 scale
--- Current progress bar size
-- @tfield vector3 size
--- Maximum size of progress bar
-- @tfield number max_size
--- Progress bar slice9 settings
-- @tfield vector4 slice
local Event = require("druid.event")
local const = require("druid.const")
local helper = require("druid.helper")
local component = require("druid.component")
local Progress = component.create("progress", { const.ON_UPDATE, const.ON_LAYOUT_CHANGE })
local function check_steps(self, from, to, exactly)
if not self.steps then
return
end
for i = 1, #self.steps do
local step = self.steps[i]
local v1, v2 = from, to
if v1 > v2 then
v1, v2 = v2, v1
end
if v1 < step and step < v2 then
self.step_callback(self:get_context(), step)
end
if exactly and exactly == step then
self.step_callback(self:get_context(), step)
end
end
end
local function set_bar_to(self, set_to, is_silent)
local prev_value = self.last_value
self.last_value = set_to
local total_width = set_to * self.max_size
local scale = math.min(total_width / self.slice_size, 1)
local size = math.max(total_width, self.slice_size)
self.scale[self.key] = scale
gui.set_scale(self.node, self.scale)
self.size[self.key] = size
gui.set_size(self.node, self.size)
if not is_silent then
check_steps(self, prev_value, set_to)
end
end
--- Component style params.
-- You can override this component styles params in druid styles table
-- or create your own style
-- @table style
-- @tfield[opt=5] number SPEED Progress bas fill rate. More -> faster
-- @tfield[opt=0.005] number MIN_DELTA Minimum step to fill progress bar
function Progress.on_style_change(self, style)
self.style = {}
self.style.SPEED = style.SPEED or 5
self.style.MIN_DELTA = style.MIN_DELTA or 0.005
end
--- Component init function
-- @tparam Progress self
-- @tparam string|node node Progress bar fill node or node name
-- @tparam string key Progress bar direction: const.SIDE.X or const.SIDE.Y
-- @tparam[opt=1] number init_value Initial value of progress bar
function Progress.init(self, node, key, init_value)
assert(key == const.SIDE.X or const.SIDE.Y, "Progress bar key should be 'x' or 'y'")
self.prop = hash("scale."..key)
self.key = key
self.node = self:get_node(node)
self.scale = gui.get_scale(self.node)
self.size = gui.get_size(self.node)
self.max_size = self.size[self.key]
self.slice = gui.get_slice9(self.node)
if key == const.SIDE.X then
self.slice_size = self.slice.x + self.slice.z
else
self.slice_size = self.slice.y + self.slice.w
end
self.on_change = Event()
self:set_to(init_value or 1)
end
function Progress.on_layout_change(self)
self:set_to(self.last_value)
end
function Progress.update(self, dt)
if self.target then
local prev_value = self.last_value
local step = math.abs(self.last_value - self.target) * (self.style.SPEED*dt)
step = math.max(step, self.style.MIN_DELTA)
self:set_to(helper.step(self.last_value, self.target, step))
if self.last_value == self.target then
check_steps(self, prev_value, self.target, self.target)
if self.target_callback then
self.target_callback(self:get_context(), self.target)
end
self.target = nil
end
end
end
--- Fill a progress bar and stop progress animation
-- @tparam Progress self
function Progress.fill(self)
set_bar_to(self, 1, true)
end
--- Empty a progress bar
-- @tparam Progress self
function Progress.empty(self)
set_bar_to(self, 0, true)
end
--- Instant fill progress bar to value
-- @tparam Progress self
-- @tparam number to Progress bar value, from 0 to 1
function Progress.set_to(self, to)
set_bar_to(self, to)
end
--- Return current progress bar value
-- @tparam Progress self
function Progress.get(self)
return self.last_value
end
--- Set points on progress bar to fire the callback
-- @tparam Progress self
-- @tparam number[] steps Array of progress bar values
-- @tparam function callback Callback on intersect step value
-- @usage progress:set_steps({0, 0.3, 0.6, 1}, function(self, step) end)
function Progress.set_steps(self, steps, callback)
self.steps = steps
self.step_callback = callback
end
--- Start animation of a progress bar
-- @tparam Progress self
-- @tparam number to value between 0..1
-- @tparam[opt] function callback Callback on animation ends
function Progress.to(self, to, callback)
to = helper.clamp(to, 0, 1)
-- cause of float error
local value = helper.round(to, 5)
if value ~= self.last_value then
self.target = value
self.target_callback = callback
else
if callback then
callback(self:get_context(), to)
end
end
end
return Progress

View File

@@ -0,0 +1,75 @@
--- Radio group module
-- @module RadioGroup
-- @within BaseComponent
-- @alias druid.radio_group
--- On any checkbox click
-- @tfield druid_event on_radio_click
--- Array of checkbox components
-- @tfield Checkbox[] checkboxes
local Event = require("druid.event")
local component = require("druid.component")
local RadioGroup = component.create("radio_group")
local function on_checkbox_click(self, index)
for i = 1, #self.checkboxes do
self.checkboxes[i]:set_state(i == index, true)
end
self.on_radio_click:trigger(self:get_context(), index)
end
--- Component init function
-- @tparam RadioGroup self
-- @tparam node[] nodes Array of gui node
-- @tparam function callback Radio callback
-- @tparam[opt=node] node[] click_nodes Array of trigger nodes, by default equals to nodes
function RadioGroup.init(self, nodes, callback, click_nodes)
self.druid = self:get_druid()
self.checkboxes = {}
self.on_radio_click = Event(callback)
for i = 1, #nodes do
local click_node = click_nodes and click_nodes[i] or nil
local checkbox = self.druid:new_checkbox(nodes[i], function()
on_checkbox_click(self, i)
end, click_node)
table.insert(self.checkboxes, checkbox)
end
end
--- Set radio group state
-- @tparam RadioGroup self
-- @tparam number index Index in radio group
function RadioGroup.set_state(self, index)
on_checkbox_click(self, index)
end
--- Return radio group state
-- @tparam RadioGroup self
-- @treturn number Index in radio group
function RadioGroup.get_state(self)
local result = -1
for i = 1, #self.checkboxes do
if self.checkboxes[i]:get_state() then
result = i
break
end
end
return result
end
return RadioGroup

171
druid/extended/slider.lua Normal file
View File

@@ -0,0 +1,171 @@
--- Druid slider component
-- @module Slider
-- @within BaseComponent
-- @alias druid.slider
--- On change value callback(self, value)
-- @tfield druid_event on_change_value
--- Slider pin node
-- @tfield node node
--- Start pin node position
-- @tfield vector3 start_pos
--- Current pin node position
-- @tfield vector3 pos
--- Targer pin node position
-- @tfield vector3 target_pos
--- End pin node position
-- @tfield vector3 end_pos
--- Length between start and end position
-- @tfield number dist
--- Current drag state
-- @tfield bool is_drag
--- Current slider value
-- @tfield number value
local Event = require("druid.event")
local helper = require("druid.helper")
local const = require("druid.const")
local component = require("druid.component")
local Slider = component.create("slider", { const.ON_INPUT_HIGH, const.ON_LAYOUT_CHANGE })
local function on_change_value(self)
self.on_change_value:trigger(self:get_context(), self.value)
end
local function set_position(self, value)
value = helper.clamp(value, 0, 1)
gui.set_position(self.node, self.start_pos + self.dist * value)
end
--- Component init function
-- @tparam Slider self
-- @tparam node node Gui pin node
-- @tparam vector3 end_pos The end position of slider
-- @tparam[opt] function callback On slider change callback
function Slider.init(self, node, end_pos, callback)
self.node = self:get_node(node)
self.start_pos = gui.get_position(self.node)
self.pos = gui.get_position(self.node)
self.target_pos = self.pos
self.end_pos = end_pos
self.dist = self.end_pos - self.start_pos
self.is_drag = false
self.value = 0
self.on_change_value = Event(callback)
assert(self.dist.x == 0 or self.dist.y == 0, "Slider for now can be only vertical or horizontal")
end
function Slider.on_layout_change(self)
self:set(self.value, true)
end
function Slider.on_input(self, action_id, action)
if action_id ~= const.ACTION_TOUCH then
return false
end
if gui.pick_node(self.node, action.x, action.y) then
if action.pressed then
self.pos = gui.get_position(self.node)
self.is_drag = true
end
end
if self.is_drag and not action.pressed then
-- move
self.pos.x = self.pos.x + action.dx
self.pos.y = self.pos.y + action.dy
local prev_x = self.target_pos.x
local prev_y = self.target_pos.y
self.target_pos.x = helper.clamp(self.pos.x, self.start_pos.x, self.end_pos.x)
self.target_pos.y = helper.clamp(self.pos.y, self.start_pos.y, self.end_pos.y)
if prev_x ~= self.target_pos.x or prev_y ~= self.target_pos.y then
local prev_value = self.value
if self.dist.x > 0 then
self.value = (self.target_pos.x - self.start_pos.x) / self.dist.x
end
if self.dist.y > 0 then
self.value = (self.target_pos.y - self.start_pos.y) / self.dist.y
end
if self.steps then
local closest_dist = 1000
local closest = nil
for i = 1, #self.steps do
local dist = math.abs(self.value - self.steps[i])
if dist < closest_dist then
closest = self.steps[i]
closest_dist = dist
end
end
if closest then
self.value = closest
end
end
if prev_value ~= self.value then
on_change_value(self)
end
end
set_position(self, self.value)
end
if action.released then
self.is_drag = false
end
return self.is_drag
end
--- Set value for slider
-- @tparam Slider self
-- @tparam number value Value from 0 to 1
-- @tparam[opt] bool is_silent Don't trigger event if true
function Slider.set(self, value, is_silent)
value = helper.clamp(value, 0, 1)
set_position(self, value)
self.value = value
if not is_silent then
on_change_value(self)
end
end
--- Set slider steps. Pin node will
-- apply closest step position
-- @tparam Slider self
-- @tparam number[] steps Array of steps
-- @usage slider:set_steps({0, 0.2, 0.6, 1})
function Slider.set_steps(self, steps)
self.steps = steps
end
return Slider

121
druid/extended/timer.lua Normal file
View File

@@ -0,0 +1,121 @@
--- Component to handle GUI timers.
-- Timer updating by game delta time. If game is not focused -
-- timer will be not updated.
-- @module Timer
-- @within BaseComponent
-- @alias druid.timer
--- On timer tick. Fire every second callback(self, value)
-- @tfield druid_event on_tick
--- On timer change enabled state callback(self, is_enabled)
-- @tfield druid_event on_set_enabled
--- On timer end callback
-- @tfield druid_event on_timer_end(self, Timer)
--- Trigger node
-- @tfield node node
--- Initial timer value
-- @tfield number from
--- Target timer value
-- @tfield number target
--- Current timer value
-- @tfield number value
local Event = require("druid.event")
local const = require("druid.const")
local formats = require("druid.helper.formats")
local helper = require("druid.helper")
local component = require("druid.component")
local Timer = component.create("timer", { const.ON_UPDATE })
--- Component init function
-- @tparam Timer self
-- @tparam node node Gui text node
-- @tparam number seconds_from Start timer value in seconds
-- @tparam[opt=0] number seconds_to End timer value in seconds
-- @tparam[opt] function callback Function on timer end
function Timer.init(self, node, seconds_from, seconds_to, callback)
self.node = self:get_node(node)
seconds_from = math.max(seconds_from, 0)
seconds_to = math.max(seconds_to or 0, 0)
self.on_tick = Event()
self.on_set_enabled = Event()
self.on_timer_end = Event(callback)
self:set_to(seconds_from)
self:set_interval(seconds_from, seconds_to)
if seconds_to - seconds_from == 0 then
self:set_state(false)
self.on_timer_end:trigger(self:get_context(), self)
end
return self
end
function Timer.update(self, dt)
if not self.is_on then
return
end
self.temp = self.temp + dt
local dist = math.min(1, math.abs(self.value - self.target))
if self.temp > dist then
self.temp = self.temp - dist
self.value = helper.step(self.value, self.target, 1)
self:set_to(self.value)
self.on_tick:trigger(self:get_context(), self.value)
if self.value == self.target then
self:set_state(false)
self.on_timer_end:trigger(self:get_context(), self)
end
end
end
--- Set text to text field
-- @tparam Timer self
-- @tparam number set_to Value in seconds
function Timer.set_to(self, set_to)
self.last_value = set_to
gui.set_text(self.node, formats.second_string_min(set_to))
end
--- Called when update
-- @tparam Timer self
-- @tparam bool is_on Timer enable state
function Timer.set_state(self, is_on)
self.is_on = is_on
self.on_set_enabled:trigger(self:get_context(), is_on)
end
--- Set time interval
-- @tparam Timer self
-- @tparam number from Start time in seconds
-- @tparam number to Target time in seconds
function Timer.set_interval(self, from, to)
self.from = from
self.value = from
self.temp = 0
self.target = to
self:set_state(true)
self:set_to(from)
end
return Timer