From 6ce2c8f85b075b5ad9e7920ba82b2b3c2a999f31 Mon Sep 17 00:00:00 2001 From: Insality Date: Fri, 17 Apr 2020 23:25:56 +0300 Subject: [PATCH 01/17] Add button click_outside event --- druid/base/button.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/druid/base/button.lua b/druid/base/button.lua index 2b2024d..f9a4dee 100644 --- a/druid/base/button.lua +++ b/druid/base/button.lua @@ -8,6 +8,7 @@ -- @tfield druid_event on_long_click (self, params, button_instance, time) On long tap button callback -- @tfield druid_event on_double_click (self, params, button_instance, click_amount) On double tap button callback -- @tfield druid_event on_hold_callback (self, params, button_instance, time) On button hold before long_click callback +-- @tfield druid_event on_click_outside (self, params, button_instance) On click outside of button --- Component fields -- @table Fields @@ -167,6 +168,7 @@ function M.init(self, node, callback, params, anim_node) self.on_long_click = Event() self.on_double_click = Event() self.on_hold_callback = Event() + self.on_click_outside = Event() end @@ -191,6 +193,9 @@ function M.on_input(self, action_id, action) if not is_pick then -- Can't interact, if touch outside of button self.can_action = false + if action.released then + self.on_click_outside:trigger(self:get_context(), self.params, self) + end return false end From b1ccdf91101b46b93c9ed4c0abcbde004d016c5b Mon Sep 17 00:00:00 2001 From: Insality Date: Fri, 17 Apr 2020 23:26:17 +0300 Subject: [PATCH 02/17] Add key events for input component --- druid/const.lua | 8 +++++--- input/game.input_binding | 14 +++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/druid/const.lua b/druid/const.lua index 7d1dcf0..5194c5a 100644 --- a/druid/const.lua +++ b/druid/const.lua @@ -4,10 +4,11 @@ local M = {} -M.ACTION_TOUCH = hash("touch") M.ACTION_TEXT = hash("text") -M.ACTION_BACKSPACE = hash("backspace") -M.ACTION_ENTER = hash("enter") +M.ACTION_TOUCH = hash("touch") +M.ACTION_MARKED_TEXT = hash("marked_text") +M.ACTION_BACKSPACE = hash("key_backspace") +M.ACTION_ENTER = hash("key_enter") M.ACTION_BACK = hash("back") @@ -68,6 +69,7 @@ M.SWIPE = { M.EMPTY_FUNCTION = function() end M.EMPTY_STRING = "" +M.SPACE_STRING = " " M.EMPTY_TABLE = {} diff --git a/input/game.input_binding b/input/game.input_binding index 8700888..ad63cf8 100644 --- a/input/game.input_binding +++ b/input/game.input_binding @@ -1,6 +1,6 @@ key_trigger { input: KEY_BACKSPACE - action: "back" + action: "key_backspace" } key_trigger { input: KEY_BACK @@ -10,7 +10,19 @@ key_trigger { input: KEY_SPACE action: "key_space" } +key_trigger { + input: KEY_ENTER + action: "key_enter" +} mouse_trigger { input: MOUSE_BUTTON_1 action: "touch" } +text_trigger { + input: TEXT + action: "text" +} +text_trigger { + input: MARKED_TEXT + action: "marked_text" +} From e06337fdee7e894104f6f5935317cd455106b61a Mon Sep 17 00:00:00 2001 From: Insality Date: Fri, 17 Apr 2020 23:26:39 +0300 Subject: [PATCH 03/17] Changed back on back and backspace. Update backhandler --- druid/base/back_handler.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/druid/base/back_handler.lua b/druid/base/back_handler.lua index 00414c8..b00779a 100644 --- a/druid/base/back_handler.lua +++ b/druid/base/back_handler.lua @@ -32,7 +32,11 @@ end -- @tparam string action_id on_input action id -- @tparam table action on_input action function M.on_input(self, action_id, action) - if action_id == const.ACTION_BACK and action[const.RELEASED] then + if not action[const.RELEASED] then + return false + end + + if action_id == const.ACTION_BACK or action_id == const.ACTION_BACKSPACE then self.on_back:trigger(self:get_context(), self.params) return true end @@ -41,4 +45,4 @@ function M.on_input(self, action_id, action) end -return M \ No newline at end of file +return M From 1e213a5f30e3c1be5d736f4f1f043f9a52e5f8dd Mon Sep 17 00:00:00 2001 From: Insality Date: Fri, 17 Apr 2020 23:26:59 +0300 Subject: [PATCH 04/17] Add text:get_text_width function --- druid/base/text.lua | 35 ++ druid/system/utf8.lua | 1045 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1080 insertions(+) create mode 100644 druid/system/utf8.lua diff --git a/druid/base/text.lua b/druid/base/text.lua index e6ccf07..1641626 100644 --- a/druid/base/text.lua +++ b/druid/base/text.lua @@ -64,6 +64,18 @@ local function update_text_area_size(self) end +-- calculate space width with font +local function get_space_width(self, font) + if not self._space_width[font] then + local no_space = gui.get_text_metrics(font, "1", 0, false, 0, 0).width + local with_space = gui.get_text_metrics(font, " 1", 0, false, 0, 0).width + self._space_width[font] = with_space - no_space + end + + return self._space_width[font] +end + + --- Component init function -- @function text:init -- @tparam node node Gui text node @@ -88,11 +100,34 @@ function M.init(self, node, value, no_adjust) self.on_set_pivot = Event() self.on_update_text_scale = Event() + self._space_width = {} + self:set_to(value or gui.get_text(self.node)) return self end +--- Calculate text width with font with respect to trailing space +-- @function text:get_text_width +-- @tparam[opt] string text +function M.get_text_width(self, text) + text = text or self.last_value + local font = gui.get_font(self.node) + local scale = gui.get_scale(self.node) + local result = gui.get_text_metrics(font, text, 0, false, 0, 0).width + for i = #text, 1, -1 do + local c = string.sub(text, i, i) + if c ~= ' ' then + break + end + + result = result + get_space_width(self, font) + end + + return result * scale.x +end + + --- Set text to text field -- @function text:set_to -- @tparam string set_to Text for node diff --git a/druid/system/utf8.lua b/druid/system/utf8.lua new file mode 100644 index 0000000..7f13914 --- /dev/null +++ b/druid/system/utf8.lua @@ -0,0 +1,1045 @@ +-- $Id: utf8.lua 179 2009-04-03 18:10:03Z pasta $ +-- +-- Provides UTF-8 aware string functions implemented in pure lua: +-- * utf8len(s) +-- * utf8sub(s, i, j) +-- * utf8reverse(s) +-- * utf8char(unicode) +-- * utf8unicode(s, i, j) +-- * utf8gensub(s, sub_len) +-- * utf8find(str, regex, init, plain) +-- * utf8match(str, regex, init) +-- * utf8gmatch(str, regex, all) +-- * utf8gsub(str, regex, repl, limit) +-- +-- If utf8data.lua (containing the lower<->upper case mappings) is loaded, these +-- additional functions are available: +-- * utf8upper(s) +-- * utf8lower(s) +-- +-- All functions behave as their non UTF-8 aware counterparts with the exception +-- that UTF-8 characters are used instead of bytes for all units. + +--[[ +Copyright (c) 2006-2007, Kyle Smith +All rights reserved. + +Contributors: + Alimov Stepan + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the author nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--]] + +-- ABNF from RFC 3629 +-- +-- UTF8-octets = *( UTF8-char ) +-- UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4 +-- UTF8-1 = %x00-7F +-- UTF8-2 = %xC2-DF UTF8-tail +-- UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) / +-- %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail ) +-- UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) / +-- %xF4 %x80-8F 2( UTF8-tail ) +-- UTF8-tail = %x80-BF +-- + +local byte = string.byte +local char = string.char +local dump = string.dump +local find = string.find +local format = string.format +local len = string.len +local lower = string.lower +local rep = string.rep +local sub = string.sub +local upper = string.upper + +-- returns the number of bytes used by the UTF-8 character at byte i in s +-- also doubles as a UTF-8 character validator +local function utf8charbytes (s, i) + -- argument defaults + i = i or 1 + + -- argument checking + if type(s) ~= "string" then + error("bad argument #1 to 'utf8charbytes' (string expected, got ".. type(s).. ")") + end + if type(i) ~= "number" then + error("bad argument #2 to 'utf8charbytes' (number expected, got ".. type(i).. ")") + end + + local c = byte(s, i) + + -- determine bytes needed for character, based on RFC 3629 + -- validate byte 1 + if c > 0 and c <= 127 then + -- UTF8-1 + return 1 + + elseif c >= 194 and c <= 223 then + -- UTF8-2 + local c2 = byte(s, i + 1) + + if not c2 then + error("UTF-8 string terminated early") + end + + -- validate byte 2 + if c2 < 128 or c2 > 191 then + error("Invalid UTF-8 character") + end + + return 2 + + elseif c >= 224 and c <= 239 then + -- UTF8-3 + local c2 = byte(s, i + 1) + local c3 = byte(s, i + 2) + + if not c2 or not c3 then + error("UTF-8 string terminated early") + end + + -- validate byte 2 + if c == 224 and (c2 < 160 or c2 > 191) then + error("Invalid UTF-8 character") + elseif c == 237 and (c2 < 128 or c2 > 159) then + error("Invalid UTF-8 character") + elseif c2 < 128 or c2 > 191 then + error("Invalid UTF-8 character") + end + + -- validate byte 3 + if c3 < 128 or c3 > 191 then + error("Invalid UTF-8 character") + end + + return 3 + + elseif c >= 240 and c <= 244 then + -- UTF8-4 + local c2 = byte(s, i + 1) + local c3 = byte(s, i + 2) + local c4 = byte(s, i + 3) + + if not c2 or not c3 or not c4 then + error("UTF-8 string terminated early") + end + + -- validate byte 2 + if c == 240 and (c2 < 144 or c2 > 191) then + error("Invalid UTF-8 character") + elseif c == 244 and (c2 < 128 or c2 > 143) then + error("Invalid UTF-8 character") + elseif c2 < 128 or c2 > 191 then + error("Invalid UTF-8 character") + end + + -- validate byte 3 + if c3 < 128 or c3 > 191 then + error("Invalid UTF-8 character") + end + + -- validate byte 4 + if c4 < 128 or c4 > 191 then + error("Invalid UTF-8 character") + end + + return 4 + + else + error("Invalid UTF-8 character") + end +end + +-- returns the number of characters in a UTF-8 string +local function utf8len (s) + -- argument checking + if type(s) ~= "string" then + for k,v in pairs(s) do print('"',tostring(k),'"',tostring(v),'"') end + error("bad argument #1 to 'utf8len' (string expected, got ".. type(s).. ")") + end + + local pos = 1 + local bytes = len(s) + local length = 0 + + while pos <= bytes do + length = length + 1 + pos = pos + utf8charbytes(s, pos) + end + + return length +end + +-- functions identically to string.sub except that i and j are UTF-8 characters +-- instead of bytes +local function utf8sub (s, i, j) + -- argument defaults + j = j or -1 + + local pos = 1 + local bytes = len(s) + local length = 0 + + -- only set l if i or j is negative + local l = (i >= 0 and j >= 0) or utf8len(s) + local startChar = (i >= 0) and i or l + i + 1 + local endChar = (j >= 0) and j or l + j + 1 + + -- can't have start before end! + if startChar > endChar then + return "" + end + + -- byte offsets to pass to string.sub + local startByte,endByte = 1,bytes + + while pos <= bytes do + length = length + 1 + + if length == startChar then + startByte = pos + end + + pos = pos + utf8charbytes(s, pos) + + if length == endChar then + endByte = pos - 1 + break + end + end + + if startChar > length then startByte = bytes+1 end + if endChar < 1 then endByte = 0 end + + return sub(s, startByte, endByte) +end + +--[[ +-- replace UTF-8 characters based on a mapping table +local function utf8replace (s, mapping) + -- argument checking + if type(s) ~= "string" then + error("bad argument #1 to 'utf8replace' (string expected, got ".. type(s).. ")") + end + if type(mapping) ~= "table" then + error("bad argument #2 to 'utf8replace' (table expected, got ".. type(mapping).. ")") + end + + local pos = 1 + local bytes = len(s) + local charbytes + local newstr = "" + + while pos <= bytes do + charbytes = utf8charbytes(s, pos) + local c = sub(s, pos, pos + charbytes - 1) + + newstr = newstr .. (mapping[c] or c) + + pos = pos + charbytes + end + + return newstr +end + + +-- identical to string.upper except it knows about unicode simple case conversions +local function utf8upper (s) + return utf8replace(s, utf8_lc_uc) +end + +-- identical to string.lower except it knows about unicode simple case conversions +local function utf8lower (s) + return utf8replace(s, utf8_uc_lc) +end +]] + +-- identical to string.reverse except that it supports UTF-8 +local function utf8reverse (s) + -- argument checking + if type(s) ~= "string" then + error("bad argument #1 to 'utf8reverse' (string expected, got ".. type(s).. ")") + end + + local bytes = len(s) + local pos = bytes + local charbytes + local newstr = "" + + while pos > 0 do + local c = byte(s, pos) + while c >= 128 and c <= 191 do + pos = pos - 1 + c = byte(s, pos) + end + + charbytes = utf8charbytes(s, pos) + + newstr = newstr .. sub(s, pos, pos + charbytes - 1) + + pos = pos - 1 + end + + return newstr +end + +-- http://en.wikipedia.org/wiki/Utf8 +-- http://developer.coronalabs.com/code/utf-8-conversion-utility +local function utf8char(unicode) + if unicode <= 0x7F then return char(unicode) end + + if (unicode <= 0x7FF) then + local Byte0 = 0xC0 + math.floor(unicode / 0x40); + local Byte1 = 0x80 + (unicode % 0x40); + return char(Byte0, Byte1); + end; + + if (unicode <= 0xFFFF) then + local Byte0 = 0xE0 + math.floor(unicode / 0x1000); + local Byte1 = 0x80 + (math.floor(unicode / 0x40) % 0x40); + local Byte2 = 0x80 + (unicode % 0x40); + return char(Byte0, Byte1, Byte2); + end; + + if (unicode <= 0x10FFFF) then + local code = unicode + local Byte3= 0x80 + (code % 0x40); + code = math.floor(code / 0x40) + local Byte2= 0x80 + (code % 0x40); + code = math.floor(code / 0x40) + local Byte1= 0x80 + (code % 0x40); + code = math.floor(code / 0x40) + local Byte0= 0xF0 + code; + + return char(Byte0, Byte1, Byte2, Byte3); + end; + + error 'Unicode cannot be greater than U+10FFFF!' +end + +local shift_6 = 2^6 +local shift_12 = 2^12 +local shift_18 = 2^18 + +local utf8unicode +utf8unicode = function(str, i, j, byte_pos) + i = i or 1 + j = j or i + + if i > j then return end + + local ch,bytes + + if byte_pos then + bytes = utf8charbytes(str,byte_pos) + ch = sub(str,byte_pos,byte_pos-1+bytes) + else + ch,byte_pos = utf8sub(str,i,i), 0 + bytes = #ch + end + + local unicode + + if bytes == 1 then unicode = byte(ch) end + if bytes == 2 then + local byte0,byte1 = byte(ch,1,2) + local code0,code1 = byte0-0xC0,byte1-0x80 + unicode = code0*shift_6 + code1 + end + if bytes == 3 then + local byte0,byte1,byte2 = byte(ch,1,3) + local code0,code1,code2 = byte0-0xE0,byte1-0x80,byte2-0x80 + unicode = code0*shift_12 + code1*shift_6 + code2 + end + if bytes == 4 then + local byte0,byte1,byte2,byte3 = byte(ch,1,4) + local code0,code1,code2,code3 = byte0-0xF0,byte1-0x80,byte2-0x80,byte3-0x80 + unicode = code0*shift_18 + code1*shift_12 + code2*shift_6 + code3 + end + + return unicode,utf8unicode(str, i+1, j, byte_pos+bytes) +end + +-- Returns an iterator which returns the next substring and its byte interval +local function utf8gensub(str, sub_len) + sub_len = sub_len or 1 + local byte_pos = 1 + local length = #str + return function(skip) + if skip then byte_pos = byte_pos + skip end + local char_count = 0 + local start = byte_pos + repeat + if byte_pos > length then return end + char_count = char_count + 1 + local bytes = utf8charbytes(str,byte_pos) + byte_pos = byte_pos+bytes + + until char_count == sub_len + + local last = byte_pos-1 + local slice = sub(str,start,last) + return slice, start, last + end +end + +local function binsearch(sortedTable, item, comp) + local head, tail = 1, #sortedTable + local mid = math.floor((head + tail)/2) + if not comp then + while (tail - head) > 1 do + if sortedTable[tonumber(mid)] > item then + tail = mid + else + head = mid + end + mid = math.floor((head + tail)/2) + end + end + if sortedTable[tonumber(head)] == item then + return true, tonumber(head) + elseif sortedTable[tonumber(tail)] == item then + return true, tonumber(tail) + else + return false + end +end +local function classMatchGenerator(class, plain) + local codes = {} + local ranges = {} + local ignore = false + local range = false + local firstletter = true + local unmatch = false + + local it = utf8gensub(class) + + local skip + for c, _, be in it do + skip = be + if not ignore and not plain then + if c == "%" then + ignore = true + elseif c == "-" then + table.insert(codes, utf8unicode(c)) + range = true + elseif c == "^" then + if not firstletter then + error('!!!') + else + unmatch = true + end + elseif c == ']' then + break + else + if not range then + table.insert(codes, utf8unicode(c)) + else + table.remove(codes) -- removing '-' + table.insert(ranges, {table.remove(codes), utf8unicode(c)}) + range = false + end + end + elseif ignore and not plain then + if c == 'a' then -- %a: represents all letters. (ONLY ASCII) + table.insert(ranges, {65, 90}) -- A - Z + table.insert(ranges, {97, 122}) -- a - z + elseif c == 'c' then -- %c: represents all control characters. + table.insert(ranges, {0, 31}) + table.insert(codes, 127) + elseif c == 'd' then -- %d: represents all digits. + table.insert(ranges, {48, 57}) -- 0 - 9 + elseif c == 'g' then -- %g: represents all printable characters except space. + table.insert(ranges, {1, 8}) + table.insert(ranges, {14, 31}) + table.insert(ranges, {33, 132}) + table.insert(ranges, {134, 159}) + table.insert(ranges, {161, 5759}) + table.insert(ranges, {5761, 8191}) + table.insert(ranges, {8203, 8231}) + table.insert(ranges, {8234, 8238}) + table.insert(ranges, {8240, 8286}) + table.insert(ranges, {8288, 12287}) + elseif c == 'l' then -- %l: represents all lowercase letters. (ONLY ASCII) + table.insert(ranges, {97, 122}) -- a - z + elseif c == 'p' then -- %p: represents all punctuation characters. (ONLY ASCII) + table.insert(ranges, {33, 47}) + table.insert(ranges, {58, 64}) + table.insert(ranges, {91, 96}) + table.insert(ranges, {123, 126}) + elseif c == 's' then -- %s: represents all space characters. + table.insert(ranges, {9, 13}) + table.insert(codes, 32) + table.insert(codes, 133) + table.insert(codes, 160) + table.insert(codes, 5760) + table.insert(ranges, {8192, 8202}) + table.insert(codes, 8232) + table.insert(codes, 8233) + table.insert(codes, 8239) + table.insert(codes, 8287) + table.insert(codes, 12288) + elseif c == 'u' then -- %u: represents all uppercase letters. (ONLY ASCII) + table.insert(ranges, {65, 90}) -- A - Z + elseif c == 'w' then -- %w: represents all alphanumeric characters. (ONLY ASCII) + table.insert(ranges, {48, 57}) -- 0 - 9 + table.insert(ranges, {65, 90}) -- A - Z + table.insert(ranges, {97, 122}) -- a - z + elseif c == 'x' then -- %x: represents all hexadecimal digits. + table.insert(ranges, {48, 57}) -- 0 - 9 + table.insert(ranges, {65, 70}) -- A - F + table.insert(ranges, {97, 102}) -- a - f + else + if not range then + table.insert(codes, utf8unicode(c)) + else + table.remove(codes) -- removing '-' + table.insert(ranges, {table.remove(codes), utf8unicode(c)}) + range = false + end + end + ignore = false + else + if not range then + table.insert(codes, utf8unicode(c)) + else + table.remove(codes) -- removing '-' + table.insert(ranges, {table.remove(codes), utf8unicode(c)}) + range = false + end + ignore = false + end + + firstletter = false + end + + table.sort(codes) + + local function inRanges(charCode) + for _,r in ipairs(ranges) do + if r[1] <= charCode and charCode <= r[2] then + return true + end + end + return false + end + if not unmatch then + return function(charCode) + return binsearch(codes, charCode) or inRanges(charCode) + end, skip + else + return function(charCode) + return charCode ~= -1 and not (binsearch(codes, charCode) or inRanges(charCode)) + end, skip + end +end + +--[[ +-- utf8sub with extra argument, and extra result value +local function utf8subWithBytes (s, i, j, sb) + -- argument defaults + j = j or -1 + + local pos = sb or 1 + local bytes = len(s) + local length = 0 + + -- only set l if i or j is negative + local l = (i >= 0 and j >= 0) or utf8len(s) + local startChar = (i >= 0) and i or l + i + 1 + local endChar = (j >= 0) and j or l + j + 1 + + -- can't have start before end! + if startChar > endChar then + return "" + end + + -- byte offsets to pass to string.sub + local startByte,endByte = 1,bytes + + while pos <= bytes do + length = length + 1 + + if length == startChar then + startByte = pos + end + + pos = pos + utf8charbytes(s, pos) + + if length == endChar then + endByte = pos - 1 + break + end + end + + if startChar > length then startByte = bytes+1 end + if endChar < 1 then endByte = 0 end + + return sub(s, startByte, endByte), endByte + 1 +end +]] + +local cache = setmetatable({},{ + __mode = 'kv' +}) +local cachePlain = setmetatable({},{ + __mode = 'kv' +}) +local function matcherGenerator(regex, plain) + local matcher = { + functions = {}, + captures = {} + } + if not plain then + cache[regex] = matcher + else + cachePlain[regex] = matcher + end + local function simple(func) + return function(cC) + if func(cC) then + matcher:nextFunc() + matcher:nextStr() + else + matcher:reset() + end + end + end + local function star(func) + return function(cC) + if func(cC) then + matcher:fullResetOnNextFunc() + matcher:nextStr() + else + matcher:nextFunc() + end + end + end + local function minus(func) + return function(cC) + if func(cC) then + matcher:fullResetOnNextStr() + end + matcher:nextFunc() + end + end + local function question(func) + return function(cC) + if func(cC) then + matcher:fullResetOnNextFunc() + matcher:nextStr() + end + matcher:nextFunc() + end + end + + local function capture(id) + return function(_) + local l = matcher.captures[id][2] - matcher.captures[id][1] + local captured = utf8sub(matcher.string, matcher.captures[id][1], matcher.captures[id][2]) + local check = utf8sub(matcher.string, matcher.str, matcher.str + l) + if captured == check then + for _ = 0, l do + matcher:nextStr() + end + matcher:nextFunc() + else + matcher:reset() + end + end + end + local function captureStart(id) + return function(_) + matcher.captures[id][1] = matcher.str + matcher:nextFunc() + end + end + local function captureStop(id) + return function(_) + matcher.captures[id][2] = matcher.str - 1 + matcher:nextFunc() + end + end + + local function balancer(str) + local sum = 0 + local bc, ec = utf8sub(str, 1, 1), utf8sub(str, 2, 2) + local skip = len(bc) + len(ec) + bc, ec = utf8unicode(bc), utf8unicode(ec) + return function(cC) + if cC == ec and sum > 0 then + sum = sum - 1 + if sum == 0 then + matcher:nextFunc() + end + matcher:nextStr() + elseif cC == bc then + sum = sum + 1 + matcher:nextStr() + else + if sum == 0 or cC == -1 then + sum = 0 + matcher:reset() + else + matcher:nextStr() + end + end + end, skip + end + + matcher.functions[1] = function(_) + matcher:fullResetOnNextStr() + matcher.seqStart = matcher.str + matcher:nextFunc() + if (matcher.str > matcher.startStr and matcher.fromStart) or matcher.str >= matcher.stringLen then + matcher.stop = true + matcher.seqStart = nil + end + end + + local lastFunc + local ignore = false + local skip = nil + local it = (function() + local gen = utf8gensub(regex) + return function() + return gen(skip) + end + end)() + local cs = {} + for c, bs, be in it do + skip = nil + if plain then + table.insert(matcher.functions, simple(classMatchGenerator(c, plain))) + else + if ignore then + if find('123456789', c, 1, true) then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + lastFunc = nil + end + table.insert(matcher.functions, capture(tonumber(c))) + elseif c == 'b' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + lastFunc = nil + end + local b + b, skip = balancer(sub(regex, be + 1, be + 9)) + table.insert(matcher.functions, b) + else + lastFunc = classMatchGenerator('%' .. c) + end + ignore = false + else + if c == '*' then + if lastFunc then + table.insert(matcher.functions, star(lastFunc)) + lastFunc = nil + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '+' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + table.insert(matcher.functions, star(lastFunc)) + lastFunc = nil + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '-' then + if lastFunc then + table.insert(matcher.functions, minus(lastFunc)) + lastFunc = nil + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '?' then + if lastFunc then + table.insert(matcher.functions, question(lastFunc)) + lastFunc = nil + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '^' then + if bs == 1 then + matcher.fromStart = true + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '$' then + if be == len(regex) then + matcher.toEnd = true + else + error('invalid regex after ' .. sub(regex, 1, bs)) + end + elseif c == '[' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + end + lastFunc, skip = classMatchGenerator(sub(regex, be + 1)) + elseif c == '(' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + lastFunc = nil + end + table.insert(matcher.captures, {}) + table.insert(cs, #matcher.captures) + table.insert(matcher.functions, captureStart(cs[#cs])) + if sub(regex, be + 1, be + 1) == ')' then matcher.captures[#matcher.captures].empty = true end + elseif c == ')' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + lastFunc = nil + end + local cap = table.remove(cs) + if not cap then + error('invalid capture: "(" missing') + end + table.insert(matcher.functions, captureStop(cap)) + elseif c == '.' then + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + end + lastFunc = function(cC) return cC ~= -1 end + elseif c == '%' then + ignore = true + else + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + end + lastFunc = classMatchGenerator(c) + end + end + end + end + if #cs > 0 then + error('invalid capture: ")" missing') + end + if lastFunc then + table.insert(matcher.functions, simple(lastFunc)) + end + + table.insert(matcher.functions, function() + if matcher.toEnd and matcher.str ~= matcher.stringLen then + matcher:reset() + else + matcher.stop = true + end + end) + + matcher.nextFunc = function(self) + self.func = self.func + 1 + end + matcher.nextStr = function(self) + self.str = self.str + 1 + end + matcher.strReset = function(self) + local oldReset = self.reset + local str = self.str + self.reset = function(s) + s.str = str + s.reset = oldReset + end + end + matcher.fullResetOnNextFunc = function(self) + local oldReset = self.reset + local func = self.func +1 + local str = self.str + self.reset = function(s) + s.func = func + s.str = str + s.reset = oldReset + end + end + matcher.fullResetOnNextStr = function(self) + local oldReset = self.reset + local str = self.str + 1 + local func = self.func + self.reset = function(s) + s.func = func + s.str = str + s.reset = oldReset + end + end + + matcher.process = function(self, str, start) + + self.func = 1 + start = start or 1 + self.startStr = (start >= 0) and start or utf8len(str) + start + 1 + self.seqStart = self.startStr + self.str = self.startStr + self.stringLen = utf8len(str) + 1 + self.string = str + self.stop = false + + self.reset = function(s) + s.func = 1 + end + + -- local lastPos = self.str + -- local lastByte + local ch + while not self.stop do + if self.str < self.stringLen then + --[[ if lastPos < self.str then + print('last byte', lastByte) + ch, lastByte = utf8subWithBytes(str, 1, self.str - lastPos - 1, lastByte) + ch, lastByte = utf8subWithBytes(str, 1, 1, lastByte) + lastByte = lastByte - 1 + else + ch, lastByte = utf8subWithBytes(str, self.str, self.str) + end + lastPos = self.str ]] + ch = utf8sub(str, self.str,self.str) + --print('char', ch, utf8unicode(ch)) + self.functions[self.func](utf8unicode(ch)) + else + self.functions[self.func](-1) + end + end + + if self.seqStart then + local captures = {} + for _,pair in pairs(self.captures) do + if pair.empty then + table.insert(captures, pair[1]) + else + table.insert(captures, utf8sub(str, pair[1], pair[2])) + end + end + return self.seqStart, self.str - 1, unpack(captures) + end + end + + return matcher +end + +-- string.find +local function utf8find(str, regex, init, plain) + local matcher = cache[regex] or matcherGenerator(regex, plain) + return matcher:process(str, init) +end + +-- string.match +local function utf8match(str, regex, init) + init = init or 1 + local found = {utf8find(str, regex, init)} + if found[1] then + if found[3] then + return unpack(found, 3) + end + return utf8sub(str, found[1], found[2]) + end +end + +-- string.gmatch +local function utf8gmatch(str, regex, all) + regex = (utf8sub(regex,1,1) ~= '^') and regex or '%' .. regex + local lastChar = 1 + return function() + local found = {utf8find(str, regex, lastChar)} + if found[1] then + lastChar = found[2] + 1 + if found[all and 1 or 3] then + return unpack(found, all and 1 or 3) + end + return utf8sub(str, found[1], found[2]) + end + end +end + +local function replace(repl, args) + local ret = '' + if type(repl) == 'string' then + local ignore = false + local num + for c in utf8gensub(repl) do + if not ignore then + if c == '%' then + ignore = true + else + ret = ret .. c + end + else + num = tonumber(c) + if num then + ret = ret .. args[num] + else + ret = ret .. c + end + ignore = false + end + end + elseif type(repl) == 'table' then + ret = repl[args[1] or args[0]] or '' + elseif type(repl) == 'function' then + if #args > 0 then + ret = repl(unpack(args, 1)) or '' + else + ret = repl(args[0]) or '' + end + end + return ret +end +-- string.gsub +local function utf8gsub(str, regex, repl, limit) + limit = limit or -1 + local ret = '' + local prevEnd = 1 + local it = utf8gmatch(str, regex, true) + local found = {it()} + local n = 0 + while #found > 0 and limit ~= n do + local args = {[0] = utf8sub(str, found[1], found[2]), unpack(found, 3)} + ret = ret .. utf8sub(str, prevEnd, found[1] - 1) + .. replace(repl, args) + prevEnd = found[2] + 1 + n = n + 1 + found = {it()} + end + return ret .. utf8sub(str, prevEnd), n +end + +local utf8 = {} +utf8.len = utf8len +utf8.sub = utf8sub +utf8.reverse = utf8reverse +utf8.char = utf8char +utf8.unicode = utf8unicode +utf8.gensub = utf8gensub +utf8.byte = utf8unicode +utf8.find = utf8find +utf8.match = utf8match +utf8.gmatch = utf8gmatch +utf8.gsub = utf8gsub +utf8.dump = dump +utf8.format = format +utf8.lower = lower +utf8.upper = upper +utf8.rep = rep +return utf8 From 08e93c7423d7dac6118f732881ce32421f9ce937 Mon Sep 17 00:00:00 2001 From: Insality Date: Fri, 17 Apr 2020 23:27:18 +0300 Subject: [PATCH 05/17] Start implementation of input component --- druid/base/input.lua | 144 ++++++++++++++++++++++- example/gui/main/main.gui | 238 +++++++++++++++++++++++++++++++++++++- example/page/main.lua | 7 ++ 3 files changed, 384 insertions(+), 5 deletions(-) diff --git a/druid/base/input.lua b/druid/base/input.lua index 603eb71..f0395dc 100644 --- a/druid/base/input.lua +++ b/druid/base/input.lua @@ -1,16 +1,152 @@ --- Druid input text component. -- Carry on user text input --- UNIMPLEMENTED +-- @author Part of code from Britzl gooey input component -- @module druid.input +local const = require("druid.const") local component = require("druid.component") +local utf8 = require("druid.system.utf8") -local M = component.create("input") +local M = component.create("input", { const.ON_INPUT }) -function M.init(self, node, callback, click_node) - self.style = self:get_style() +local function select(self) + gui.reset_keyboard() + if not self.selected then + print("selected") + self.selected = true + gui.show_keyboard(gui.KEYBOARD_TYPE_DEFAULT, false) + end end +local function unselect(self) + gui.reset_keyboard() + if self.selected then + self.selected = false + print("unselected") + gui.hide_keyboard() + end +end + + +function M.init(self, click_node, text_node) + self.druid = self:get_druid(self) + self.text = self.druid:new_text(text_node) + + self.selected = false + self.value = "" + self.marked_value = "" + self.current_value = "" + self.is_empty = true + + self.text_width = 0 + self.market_text_width = 0 + self.total_width = 0 + + self.max_width = 10 + + self.keyboard_type = gui.KEYBOARD_TYPE_DEFAULT + + self.button = self.druid:new_button(click_node, select) + self.button.on_click_outside:subscribe(unselect) +end + + +function M.on_input(self, action_id, action) + if self.selected then + local input_text = nil + if action_id == const.ACTION_TEXT then + print("usual", action.text) + -- 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 config or not config.allowed_characters or action.text:match(config.allowed_characters) then + input_text = self.value .. action.text + if self.max_length then + input_text = utf8.sub(self.value, 1, self.max_length) + end + self.marked_value = "" + end + end + + if action_id == const.ACTION_MARKED_TEXT then + print("marked") + self.marked_value = action.text or "" + if self.max_length then + input_text = 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 input_text then + print("set input_text", input_text) + self:set_text(input_text) + return true + end + end + + return self.selected +end + + +function M.set_text(self, input_text) + self.value = input_text + + -- only update the text if it has changed + local current_value = self.value .. self.marked_value + + print(self.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 + masked_value = M.mask_text(self.text, "*") + masked_marked_value = M.mask_text(self.marked_text, "*") + 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 + + -- 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.text:set_to(value .. marked_value) + end +end + + +function M.get_text(self) + return self.value .. self.marked_value +end + + + return M diff --git a/example/gui/main/main.gui b/example/gui/main/main.gui index 5f58f85..5c98da4 100644 --- a/example/gui/main/main.gui +++ b/example/gui/main/main.gui @@ -2882,7 +2882,7 @@ nodes { nodes { position { x: 0.0 - y: -800.0 + y: -890.0 z: 0.0 w: 1.0 } @@ -2989,6 +2989,242 @@ nodes { template_node_child: false size_mode: SIZE_MODE_MANUAL } +nodes { + position { + x: 0.0 + y: -800.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 1.0 + y: 1.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "kenney/empty" + id: "section_input" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "scroll_content" + layer: "image" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_AUTO +} +nodes { + position { + x: -250.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.7 + y: 0.7 + z: 1.0 + w: 1.0 + } + size { + x: 300.0 + y: 60.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Input:" + font: "game" + id: "text_input" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_W + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "section_input" + layer: "text" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 0.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 130.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 190.0 + y: 45.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "kenney/progress_back" + id: "input_box" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "section_input" + layer: "image" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_AUTO +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.4 + y: 0.4 + z: 1.0 + w: 1.0 + } + size { + x: 450.0 + y: 80.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Input text will be here" + font: "game" + id: "input_text" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "input_box" + layer: "" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 0.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} nodes { position { x: 600.0 diff --git a/example/page/main.lua b/example/page/main.lua index 2da7d7f..a56eab1 100644 --- a/example/page/main.lua +++ b/example/page/main.lua @@ -106,6 +106,12 @@ local function setup_back_handler(self) end +local function setup_input(self) + local input = self.druid:new_input("input_box", "input_text") + input:set_text("hello!") +end + + function M.setup_page(self) setup_texts(self) @@ -117,6 +123,7 @@ function M.setup_page(self) setup_scroll(self) setup_slider(self) setup_back_handler(self) + setup_input(self) end From be26478c212ddc1249ee6defa2665e1a40c86709 Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 00:45:18 +0300 Subject: [PATCH 06/17] Input implementation progress --- docs_md/changelog.md | 13 ++++++++ druid/base/button.lua | 1 - druid/base/input.lua | 55 ++++++++++++++++++++++++---------- druid/base/text.lua | 8 +++++ druid/const.lua | 2 ++ druid/styles/default/style.lua | 27 ++++++++++++++++- druid/styles/empty/style.lua | 1 - example/gui/main/main.gui | 10 +++---- game.project | 3 ++ input/game.input_binding | 8 +++++ 10 files changed, 105 insertions(+), 23 deletions(-) create mode 100644 docs_md/changelog.md diff --git a/docs_md/changelog.md b/docs_md/changelog.md new file mode 100644 index 0000000..c3610fc --- /dev/null +++ b/docs_md/changelog.md @@ -0,0 +1,13 @@ +Druid 0.3.0: + +- Add swipe basic component + - Swipe component handle simple swipe gestures on node. It has single callback with direction on swipe. You can adjust a several parameters of swipe in druid style. + +- Add input basic component + - Input component handle user text input. Input contains from button and text component. Button needed for selecting input field + +- Add button on_click_outside event. You can subscribe on this event in button. Was needed for Input component (click outside to deselect input field). + +- Changed input binding settings. Add backspace, enter, text and marked_text. Backspace now is different from android back button. + +- Add several examples to druid-assets \ No newline at end of file diff --git a/druid/base/button.lua b/druid/base/button.lua index f9a4dee..05f47d8 100644 --- a/druid/base/button.lua +++ b/druid/base/button.lua @@ -26,7 +26,6 @@ -- @tfield function on_click_disabled (self, node) -- @tfield function on_hover (self, node, hover_state) -- @tfield function on_set_enabled (self, node, enabled_state) --- @tfield bool IS_HOVER local Event = require("druid.event") local const = require("druid.const") diff --git a/druid/base/input.lua b/druid/base/input.lua index f0395dc..ae1cbba 100644 --- a/druid/base/input.lua +++ b/druid/base/input.lua @@ -3,6 +3,7 @@ -- @author Part of code from Britzl gooey input component -- @module druid.input +local Event = require("druid.event") local const = require("druid.const") local component = require("druid.component") local utf8 = require("druid.system.utf8") @@ -12,26 +13,37 @@ local M = component.create("input", { const.ON_INPUT }) local function select(self) gui.reset_keyboard() + self.marked_value = "" if not self.selected then - print("selected") self.selected = true gui.show_keyboard(gui.KEYBOARD_TYPE_DEFAULT, false) + self.on_input_select:trigger(self:get_context()) + + if self.style.on_select then + self.style.on_select(self) + end end end local function unselect(self) gui.reset_keyboard() + self.marked_value = "" if self.selected then self.selected = false - print("unselected") gui.hide_keyboard() + self.on_input_unselect:trigger(self:get_context()) + + if self.style.on_unselect then + self.style.on_unselect(self) + end end end -function M.init(self, click_node, text_node) +function M.init(self, click_node, text_node, keyboard_type) self.druid = self:get_druid(self) + self.style = self:get_style(self) self.text = self.druid:new_text(text_node) self.selected = false @@ -44,12 +56,20 @@ function M.init(self, click_node, text_node) self.market_text_width = 0 self.total_width = 0 - self.max_width = 10 + self.max_length = 18 + self.allowed_characters = nil - self.keyboard_type = gui.KEYBOARD_TYPE_DEFAULT + self.keyboard_type = keyboard_type or gui.KEYBOARD_TYPE_DEFAULT self.button = self.druid:new_button(click_node, select) + self.button:set_style(self.style) self.button.on_click_outside:subscribe(unselect) + + 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() end @@ -57,7 +77,6 @@ function M.on_input(self, action_id, action) if self.selected then local input_text = nil if action_id == const.ACTION_TEXT then - print("usual", action.text) -- ignore return key if action.text == "\n" or action.text == "\r" then return true @@ -69,17 +88,17 @@ function M.on_input(self, action_id, action) -- ignore arrow keys if not string.match(hex, "EF9C8[0-3]") then - -- if not config or not config.allowed_characters or action.text:match(config.allowed_characters) then - input_text = self.value .. action.text - if self.max_length then - input_text = utf8.sub(self.value, 1, self.max_length) + 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 end self.marked_value = "" end end if action_id == const.ACTION_MARKED_TEXT then - print("marked") self.marked_value = action.text or "" if self.max_length then input_text = utf8.sub(self.marked_value, 1, self.max_length) @@ -101,7 +120,6 @@ function M.on_input(self, action_id, action) end if input_text then - print("set input_text", input_text) self:set_text(input_text) return true end @@ -117,7 +135,6 @@ function M.set_text(self, input_text) -- only update the text if it has changed local current_value = self.value .. self.marked_value - print(self.value) if current_value ~= self.current_value then self.current_value = current_value @@ -138,7 +155,16 @@ function M.set_text(self, input_text) self.marked_text_width = self.text:get_text_width(marked_value) self.total_width = self.text_width + self.marked_text_width - self.text:set_to(value .. marked_value) + local final_text = value .. marked_value + self.text:set_to(final_text) + + 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 @@ -148,5 +174,4 @@ function M.get_text(self) end - return M diff --git a/druid/base/text.lua b/druid/base/text.lua index 1641626..5a668ee 100644 --- a/druid/base/text.lua +++ b/druid/base/text.lua @@ -194,4 +194,12 @@ function M.set_pivot(self, pivot) end +--- Return true, if text with line break +-- @function text:is_multiline +-- @treturn boolean Is text node with line break +function M.is_multiline(self) + return gui.get_line_break(self.node) +end + + return M diff --git a/druid/const.lua b/druid/const.lua index 5194c5a..e94ebdc 100644 --- a/druid/const.lua +++ b/druid/const.lua @@ -10,6 +10,8 @@ M.ACTION_MARKED_TEXT = hash("marked_text") M.ACTION_BACKSPACE = hash("key_backspace") M.ACTION_ENTER = hash("key_enter") M.ACTION_BACK = hash("back") +M.ACTION_SCROLL_UP = hash("scroll_up") +M.ACTION_SCROLL_DOWN = hash("scroll_down") M.RELEASED = "released" diff --git a/druid/styles/default/style.lua b/druid/styles/default/style.lua index 12164e3..8fa39cf 100644 --- a/druid/styles/default/style.lua +++ b/druid/styles/default/style.lua @@ -15,7 +15,6 @@ M["button"] = { LONGTAP_TIME = 0.4, AUTOHOLD_TRIGGER = 0.8, DOUBLETAP_TIME = 0.4, - IS_HOVER = true, on_hover = function(self, node, state) local scale_to = self.start_scale + M.button.HOVER_SCALE @@ -51,6 +50,7 @@ M["scroll"] = { INERT_SPEED = 25, -- koef. of inert speed DEADZONE = 6, -- in px SOFT_ZONE_SIZE = 160, -- size of outside zone (back move) + SCROLL_WHEEL_SPEED = 10, BACK_SPEED = 0.2, -- lerp speed ANIM_SPEED = 0.3, -- gui.animation speed to point } @@ -82,4 +82,29 @@ M["swipe"] = { } +M["input"] = { + BUTTON_SELECT_INCREASE = 1.1, + on_select = function(self) + local button = self.button.node + local target_scale = self.button.start_scale + gui.animate(button, "scale", target_scale * M.input.BUTTON_SELECT_INCREASE, gui.EASING_OUTSINE, 0.15) + end, + on_unselect = function(self) + local button = self.button.node + local start_scale = self.button.start_scale + gui.animate(button, "scale", start_scale, gui.EASING_OUTSINE, 0.15) + end, + + button = { + BTN_SOUND = "click", + BTN_SOUND_DISABLED = "click", + DISABLED_COLOR = vmath.vector4(0, 0, 0, 1), + ENABLED_COLOR = vmath.vector4(1), + LONGTAP_TIME = 0.4, + AUTOHOLD_TRIGGER = 0.8, + DOUBLETAP_TIME = 0.4, + } +} + + return M diff --git a/druid/styles/empty/style.lua b/druid/styles/empty/style.lua index 3ede1b5..137c805 100644 --- a/druid/styles/empty/style.lua +++ b/druid/styles/empty/style.lua @@ -8,7 +8,6 @@ M["button"] = { ENABLED_COLOR = vmath.vector4(1), LONGTAP_TIME = 0.4, DOUBLETAP_TIME = 0.4, - IS_HOVER = false, } diff --git a/example/gui/main/main.gui b/example/gui/main/main.gui index 5c98da4..c46c188 100644 --- a/example/gui/main/main.gui +++ b/example/gui/main/main.gui @@ -3176,13 +3176,13 @@ nodes { w: 1.0 } scale { - x: 0.4 - y: 0.4 + x: 0.7 + y: 0.7 z: 1.0 w: 1.0 } size { - x: 450.0 + x: 250.0 y: 80.0 z: 0.0 w: 1.0 @@ -3195,7 +3195,7 @@ nodes { } type: TYPE_TEXT blend_mode: BLEND_MODE_ALPHA - text: "Input text will be here" + text: "Hello" font: "game" id: "input_text" xanchor: XANCHOR_NONE @@ -3214,7 +3214,7 @@ nodes { w: 1.0 } adjust_mode: ADJUST_MODE_FIT - line_break: false + line_break: true parent: "input_box" layer: "" inherit_alpha: true diff --git a/game.project b/game.project index 4b10954..0d0991d 100644 --- a/game.project +++ b/game.project @@ -36,3 +36,6 @@ app_manifest = /example/game.appmanifest [graphics] texture_profiles = /example/custom.texture_profiles +[android] +package = com.insality.druid + diff --git a/input/game.input_binding b/input/game.input_binding index ad63cf8..9e1054a 100644 --- a/input/game.input_binding +++ b/input/game.input_binding @@ -18,6 +18,14 @@ mouse_trigger { input: MOUSE_BUTTON_1 action: "touch" } +mouse_trigger { + input: MOUSE_WHEEL_UP + action: "scroll_up" +} +mouse_trigger { + input: MOUSE_WHEEL_DOWN + action: "scroll_down" +} text_trigger { input: TEXT action: "text" From 49e915d78bbc5844f526ec4a29530c6dd3fc2d72 Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 01:39:09 +0300 Subject: [PATCH 07/17] Clear input field on long tap. Unselect on focus lost --- druid/base/input.lua | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/druid/base/input.lua b/druid/base/input.lua index ae1cbba..3682c49 100644 --- a/druid/base/input.lua +++ b/druid/base/input.lua @@ -8,7 +8,7 @@ local const = require("druid.const") local component = require("druid.component") local utf8 = require("druid.system.utf8") -local M = component.create("input", { const.ON_INPUT }) +local M = component.create("input", { const.ON_INPUT, const.ON_FOCUS_LOST }) local function select(self) @@ -16,7 +16,7 @@ local function select(self) self.marked_value = "" if not self.selected then self.selected = true - gui.show_keyboard(gui.KEYBOARD_TYPE_DEFAULT, false) + gui.show_keyboard(self.keyboard_type, false) self.on_input_select:trigger(self:get_context()) if self.style.on_select then @@ -41,6 +41,12 @@ local function unselect(self) end +local function clear_and_select(self) + self:set_text("") + select(self) +end + + function M.init(self, click_node, text_node, keyboard_type) self.druid = self:get_druid(self) self.style = self:get_style(self) @@ -59,11 +65,12 @@ function M.init(self, click_node, text_node, keyboard_type) self.max_length = 18 self.allowed_characters = nil - self.keyboard_type = keyboard_type or gui.KEYBOARD_TYPE_DEFAULT + self.keyboard_type = keyboard_type or gui.KEYBOARD_TYPE_NUMBER_PAD self.button = self.druid:new_button(click_node, select) self.button:set_style(self.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() @@ -129,6 +136,11 @@ function M.on_input(self, action_id, action) end +function M.on_focus_lost(self) + unselect(self) +end + + function M.set_text(self, input_text) self.value = input_text From b9b67c55d6af71fe178b9129a8ddc7caddde3a55 Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 01:40:38 +0300 Subject: [PATCH 08/17] Renamed interests --- druid/base/lang_text.lua | 4 ++-- druid/const.lua | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/druid/base/lang_text.lua b/druid/base/lang_text.lua index 5c81c03..f6a90ce 100644 --- a/druid/base/lang_text.lua +++ b/druid/base/lang_text.lua @@ -15,7 +15,7 @@ local const = require("druid.const") local settings = require("druid.system.settings") local component = require("druid.component") -local M = component.create("lang_text", { const.ON_CHANGE_LANGUAGE }) +local M = component.create("lang_text", { const.ON_LANGUAGE_CHANGE }) --- Component init function @@ -35,7 +35,7 @@ function M.init(self, node, locale_id, no_adjust) end -function M.on_change_language(self) +function M.on_language_change(self) if self.last_locale then M.translate(self) end diff --git a/druid/const.lua b/druid/const.lua index e94ebdc..316991d 100644 --- a/druid/const.lua +++ b/druid/const.lua @@ -27,8 +27,10 @@ M.ON_MESSAGE = hash("on_message") M.ON_UPDATE = hash("on_update") M.ON_INPUT_HIGH = hash("on_input_high") M.ON_INPUT = hash("on_input") -M.ON_CHANGE_LANGUAGE = hash("on_change_language") -M.ON_LAYOUT_CHANGED = hash("on_layout_changed") +M.ON_LANGUAGE_CHANGE = hash("on_language_change") +M.ON_LAYOUT_CHANGE = hash("on_layout_change") +M.ON_FOCUS_LOST = hash("on_focus_lost") +M.ON_FOCUS_GAINED = hash("on_focus_gained") M.PIVOTS = { @@ -45,8 +47,10 @@ M.PIVOTS = { M.SPECIFIC_UI_MESSAGES = { - [M.ON_CHANGE_LANGUAGE] = "on_change_language", - [M.ON_LAYOUT_CHANGED] = "on_layout_changed" + [M.ON_LANGUAGE_CHANGE] = "on_language_change", + [M.ON_LAYOUT_CHANGE] = "on_layout_change", + [M.ON_FOCUS_LOST] = "on_focus_lost", + [M.ON_FOCUS_GAINED] = "on_focus_gained", } From f02c68242a0e97b59f2c56992dbb147dfed1b2db Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 01:41:28 +0300 Subject: [PATCH 09/17] Add list of druid instances, add global events: on_window_callback, on_layout_change and on_language_change --- druid/druid.lua | 66 ++++++++++++++++++++++++++++++-- druid/system/druid_instance.lua | 43 +++++++++++++++++++++ example/gui/main/main.gui_script | 7 ++++ example/init.script | 5 +-- example/lang.lua | 4 +- 5 files changed, 115 insertions(+), 10 deletions(-) diff --git a/druid/druid.lua b/druid/druid.lua index 3fb795d..2bbcd43 100644 --- a/druid/druid.lua +++ b/druid/druid.lua @@ -22,6 +22,17 @@ local default_style = require("druid.styles.default.style") local M = {} +local _instances = {} + +local function get_druid_instances() + for i = #_instances, 1, -1 do + if _instances[i]._deleted then + table.remove(_instances, i) + end + end + + return _instances +end --- Register external druid component. -- After register you can create the component with @@ -47,11 +58,14 @@ function M.new(context, style) if settings.default_style == nil then M.set_default_style(default_style) end - return druid_instance(context, style) + + local new_instance = druid_instance(context, style) + table.insert(_instances, new_instance) + return new_instance end --- Set new default style. +--- Set new default style. -- @function druid.set_default_style -- @tparam table style Druid style module function M.set_default_style(style) @@ -59,7 +73,7 @@ function M.set_default_style(style) end --- Set text function. +--- Set text function. -- Druid locale component will call this function -- to get translated text. After set_text_funtion -- all existing locale component will be updated @@ -72,7 +86,7 @@ function M.set_text_function(callback) end --- Set sound function. +--- Set sound function. -- Component will call this function to -- play sound by sound_id -- @function druid.set_sound_function @@ -82,4 +96,48 @@ function M.set_sound_function(callback) end +--- Callback on global window event. +-- Used to trigger on_focus_lost and on_focus_gain +-- @function druid.on_window_callback +-- @tparam string event Event param from window listener +-- @tparam table data Data param from window listener +function M.on_window_callback(event) + local instances = get_druid_instances() + + if event == window.WINDOW_EVENT_FOCUS_LOST then + for i = 1, #instances do + msg.post(instances[i].url, const.ON_FOCUS_LOST) + end + end + + if event == window.WINDOW_EVENT_FOCUS_GAINED then + for i = 1, #instances do + msg.post(instances[i].url, const.ON_FOCUS_GAINED) + end + end +end + + +--- Callback on global layout change event. +-- @function druid.on_layout_change +function M.on_layout_change() + local instances = get_druid_instances() + + for i = 1, #instances do + msg.post(instances[i].url, const.ON_LAYOUT_CHANGE) + end +end + + +--- Callback on global language change event. +-- Used to update all lang texts +-- @function druid.on_language_change +function M.on_language_change() + local instances = get_druid_instances() + + for i = 1, #instances do + msg.post(instances[i].url, const.ON_LANGUAGE_CHANGE) + end +end + return M diff --git a/druid/system/druid_instance.lua b/druid/system/druid_instance.lua index f071608..5a5fb4a 100644 --- a/druid/system/druid_instance.lua +++ b/druid/system/druid_instance.lua @@ -111,6 +111,8 @@ end function Druid.initialize(self, context, style) self._context = context self._style = style or settings.default_style + self._deleted = false + self.url = msg.url() self.components = {} end @@ -141,6 +143,8 @@ function Druid.final(self) components[i]:on_remove() end end + + self._deleted = true end @@ -231,6 +235,45 @@ function Druid.on_message(self, message_id, message, sender) end +function Druid.on_focus_lost(self) + local components = self.components[const.ON_FOCUS_LOST] + if components then + for i = 1, #components do + components[i]:on_focus_lost() + end + end +end + +function Druid.on_focus_gained(self) + local components = self.components[const.ON_FOCUS_GAINED] + if components then + for i = 1, #components do + components[i]:on_focus_gained() + end + end +end + + +function Druid.on_layout_change(self) + local components = self.components[const.ON_LAYOUT_CHANGE] + if components then + for i = 1, #components do + components[i]:on_layout_change() + end + end +end + + +function Druid.on_language_change(self) + local components = self.components[const.ON_LANGUAGE_CHANGE] + if components then + for i = 1, #components do + components[i]:on_language_change() + end + end +end + + --- Create button basic component -- @function druid:new_button -- @tparam args ... button init args diff --git a/example/gui/main/main.gui_script b/example/gui/main/main.gui_script index c08c365..275561f 100644 --- a/example/gui/main/main.gui_script +++ b/example/gui/main/main.gui_script @@ -51,10 +51,17 @@ local function init_swipe_control(self) end +local function on_window_callback(self, event, data) + druid.on_window_callback(event, data) +end + + function init(self) druid.set_default_style(default_style) self.druid = druid.new(self) + window.set_listener(on_window_callback) + init_top_panel(self) init_swipe_control(self) self.page = 1 diff --git a/example/init.script b/example/init.script index 87e6385..b2bfc47 100644 --- a/example/init.script +++ b/example/init.script @@ -1,5 +1,4 @@ local druid = require("druid.druid") -local const = require("druid.const") local lang = require("example.lang") @@ -12,9 +11,7 @@ local function setup_druid() return lang.get_locale(lang_id) end) - -- TODO: Call druid.finish_setup? - -- Need to update all gui, in case, when gui init was befure this init - msg.post("/gui#main", const.ON_CHANGE_LANGUAGE) + druid.on_language_change() end diff --git a/example/lang.lua b/example/lang.lua index addbb3d..890a64d 100644 --- a/example/lang.lua +++ b/example/lang.lua @@ -1,4 +1,4 @@ -local const = require("druid.const") +local druid = require("druid.druid") local M = {} @@ -47,7 +47,7 @@ end function M.toggle_locale() data = data == en and ru or en - msg.post("/gui#main", const.ON_CHANGE_LANGUAGE) + druid.on_language_change() end return M From 6d84f06c3237c1f64cbfddf157c05e149f18f1a5 Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 02:01:45 +0300 Subject: [PATCH 10/17] Add on_input_wrong, add allowerd_characters, add simple wrong animation --- docs_md/changelog.md | 17 ++++++++++++++++- druid/base/button.lua | 2 ++ druid/base/input.lua | 23 ++++++++++++++++++++--- druid/styles/default/style.lua | 21 +++++++++++++++------ 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/docs_md/changelog.md b/docs_md/changelog.md index c3610fc..a103196 100644 --- a/docs_md/changelog.md +++ b/docs_md/changelog.md @@ -1,13 +1,28 @@ Druid 0.3.0: +- Druid:final now is important function for correct working + - Add swipe basic component - Swipe component handle simple swipe gestures on node. It has single callback with direction on swipe. You can adjust a several parameters of swipe in druid style. - Add input basic component - Input component handle user text input. Input contains from button and text component. Button needed for selecting input field + - Long click on input field for clear and select input field + - Click outside of button to unselect input field + - On focus lost (game minimized) input field will be unselected + - You can setup max length of the text + - You can setup allowed characters. On add not allowed characters `on_input_wrong` will be called. By default it cause simple shake animation - Add button on_click_outside event. You can subscribe on this event in button. Was needed for Input component (click outside to deselect input field). +- Add start_pos to button component - Changed input binding settings. Add backspace, enter, text and marked_text. Backspace now is different from android back button. -- Add several examples to druid-assets \ No newline at end of file +- Changed component interest: Renamed on_change_language -> on_language_change +- Add two new component interests: on_focus_gain and on_focus_lost +- Add global druid events: + - on_window_callback: call `druid.on_window_callback(event)` for on_focus_gain/lost correct work + - on_language_change: call `druid.on_language_change()` for update all druid instances lang components + - on_layout_change: call `druid.on_layout_change()` for update all gui layouts (unsupported now) + +- Add several examples to druid-assets respository diff --git a/druid/base/button.lua b/druid/base/button.lua index 05f47d8..9d6bc32 100644 --- a/druid/base/button.lua +++ b/druid/base/button.lua @@ -15,6 +15,7 @@ -- @tfield node node Trigger node -- @tfield[opt=node] node anim_node Animation node -- @tfield vector3 start_scale Initial scale of anim_node +-- @tfield vector3 start_pos Initial pos of anim_node -- @tfield vector3 pos Initial pos of anim_node -- @tfield any params Params to click callbacks -- @tfield druid.hover hover Druid hover logic component @@ -152,6 +153,7 @@ function M.init(self, node, callback, params, anim_node) self.anim_node = anim_node and helper:get_node(anim_node) or self.node self.start_scale = gui.get_scale(self.anim_node) + self.start_pos = gui.get_position(self.anim_node) self.params = params self.hover = self.druid:new_hover(node, on_button_hover) self.click_zone = nil diff --git a/druid/base/input.lua b/druid/base/input.lua index 3682c49..c11b4d2 100644 --- a/druid/base/input.lua +++ b/druid/base/input.lua @@ -20,7 +20,7 @@ local function select(self) self.on_input_select:trigger(self:get_context()) if self.style.on_select then - self.style.on_select(self) + self.style.on_select(self, self.button.node) end end end @@ -35,7 +35,7 @@ local function unselect(self) self.on_input_unselect:trigger(self:get_context()) if self.style.on_unselect then - self.style.on_unselect(self) + self.style.on_unselect(self, self.button.node) end end end @@ -62,7 +62,7 @@ function M.init(self, click_node, text_node, keyboard_type) self.market_text_width = 0 self.total_width = 0 - self.max_length = 18 + self.max_length = nil self.allowed_characters = nil self.keyboard_type = keyboard_type or gui.KEYBOARD_TYPE_NUMBER_PAD @@ -77,6 +77,7 @@ function M.init(self, click_node, text_node, keyboard_type) self.on_input_text = Event() self.on_input_empty = Event() self.on_input_full = Event() + self.on_input_wrong = Event() end @@ -100,6 +101,11 @@ function M.on_input(self, action_id, action) 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) + if self.style.on_input_wrong then + self.style.on_input_wrong(self, self.button.node) + end end self.marked_value = "" end @@ -186,4 +192,15 @@ function M.get_text(self) end +function M.set_max_length(self, max_length) + self.max_length = max_length +end + + +-- [%a%d] for alpha numeric +function M.set_allowed_characters(self, characters) + self.allowed_characters = characters +end + + return M diff --git a/druid/styles/default/style.lua b/druid/styles/default/style.lua index 8fa39cf..7e40eb8 100644 --- a/druid/styles/default/style.lua +++ b/druid/styles/default/style.lua @@ -84,15 +84,24 @@ M["swipe"] = { M["input"] = { BUTTON_SELECT_INCREASE = 1.1, - on_select = function(self) - local button = self.button.node + + on_select = function(self, button_node) local target_scale = self.button.start_scale - gui.animate(button, "scale", target_scale * M.input.BUTTON_SELECT_INCREASE, gui.EASING_OUTSINE, 0.15) + gui.animate(button_node, "scale", target_scale * M.input.BUTTON_SELECT_INCREASE, gui.EASING_OUTSINE, 0.15) end, - on_unselect = function(self) - local button = self.button.node + + on_unselect = function(self, button_node) local start_scale = self.button.start_scale - gui.animate(button, "scale", start_scale, gui.EASING_OUTSINE, 0.15) + gui.animate(button_node, "scale", start_scale, gui.EASING_OUTSINE, 0.15) + end, + + on_input_wrong = function(self, button_node) + local start_pos = self.button.start_pos + gui.animate(button_node, "position.x", start_pos.x - 3, gui.EASING_OUTSINE, 0.05, 0, function() + gui.animate(button_node, "position.x", start_pos.x + 3, gui.EASING_OUTSINE, 0.1, 0, function() + gui.animate(button_node, "position.x", start_pos.x, gui.EASING_OUTSINE, 0.05) + end) + end) end, button = { From 56795363f86cb62c4437028fd53897a4b6d402ad Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 02:08:56 +0300 Subject: [PATCH 11/17] Add esc button to unselect input field --- druid/base/input.lua | 13 +++++++++++++ druid/const.lua | 1 + input/game.input_binding | 4 ++++ 3 files changed, 18 insertions(+) diff --git a/druid/base/input.lua b/druid/base/input.lua index c11b4d2..69b665a 100644 --- a/druid/base/input.lua +++ b/druid/base/input.lua @@ -15,6 +15,7 @@ local function select(self) gui.reset_keyboard() self.marked_value = "" if not self.selected then + self.previous_value = self.value self.selected = true gui.show_keyboard(self.keyboard_type, false) self.on_input_select:trigger(self:get_context()) @@ -53,6 +54,7 @@ function M.init(self, click_node, text_node, keyboard_type) self.text = self.druid:new_text(text_node) self.selected = false + self.previous_value = "" self.value = "" self.marked_value = "" self.current_value = "" @@ -132,6 +134,11 @@ function M.on_input(self, action_id, action) return true end + if action_id == const.ACTION_ESC and action.released then + unselect(self) + return true + end + if input_text then self:set_text(input_text) return true @@ -203,4 +210,10 @@ function M.set_allowed_characters(self, characters) end +function M.reset_changes(self) + self:set_text(self.previous_value) + unselect(self) +end + + return M diff --git a/druid/const.lua b/druid/const.lua index 316991d..4af54b2 100644 --- a/druid/const.lua +++ b/druid/const.lua @@ -10,6 +10,7 @@ M.ACTION_MARKED_TEXT = hash("marked_text") M.ACTION_BACKSPACE = hash("key_backspace") M.ACTION_ENTER = hash("key_enter") M.ACTION_BACK = hash("back") +M.ACTION_ESC = hash("key_esc") M.ACTION_SCROLL_UP = hash("scroll_up") M.ACTION_SCROLL_DOWN = hash("scroll_down") diff --git a/input/game.input_binding b/input/game.input_binding index 9e1054a..1c965b4 100644 --- a/input/game.input_binding +++ b/input/game.input_binding @@ -14,6 +14,10 @@ key_trigger { input: KEY_ENTER action: "key_enter" } +key_trigger { + input: KEY_ESC + action: "key_esc" +} mouse_trigger { input: MOUSE_BUTTON_1 action: "touch" From 36d7bcee5a4a663e4c15fac6784f79715a3580fa Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 02:31:52 +0300 Subject: [PATCH 12/17] Update docs and changelogs --- docs_md/changelog.md | 2 ++ druid/base/input.lua | 17 ++++++++++++++++- druid/druid.lua | 3 +-- druid/system/druid_instance.lua | 14 ++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/docs_md/changelog.md b/docs_md/changelog.md index a103196..a70a539 100644 --- a/docs_md/changelog.md +++ b/docs_md/changelog.md @@ -4,6 +4,7 @@ Druid 0.3.0: - Add swipe basic component - Swipe component handle simple swipe gestures on node. It has single callback with direction on swipe. You can adjust a several parameters of swipe in druid style. + - Add swipe example at main Druid example. Try swipe left/right to switch example pages. - Add input basic component - Input component handle user text input. Input contains from button and text component. Button needed for selecting input field @@ -12,6 +13,7 @@ Druid 0.3.0: - On focus lost (game minimized) input field will be unselected - You can setup max length of the text - You can setup allowed characters. On add not allowed characters `on_input_wrong` will be called. By default it cause simple shake animation + - The keyboard for input will not show on mobile HTML5 - Add button on_click_outside event. You can subscribe on this event in button. Was needed for Input component (click outside to deselect input field). - Add start_pos to button component diff --git a/druid/base/input.lua b/druid/base/input.lua index 69b665a..81c8193 100644 --- a/druid/base/input.lua +++ b/druid/base/input.lua @@ -154,6 +154,9 @@ function M.on_focus_lost(self) end +--- Set text for input field +-- @function input:set_text +-- @tparam string input_text The string to apply for input field function M.set_text(self, input_text) self.value = input_text @@ -194,22 +197,34 @@ function M.set_text(self, input_text) end +--- Return current input field text +-- @function input:get_text +-- @treturn string The current input field text function M.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) +-- @function input:set_max_length +-- @tparam number max_length Maximum length for input text field function M.set_max_length(self, max_length) self.max_length = max_length end --- [%a%d] for alpha numeric +--- Set allowed charaters for input field. +-- ex: [%a%d] for alpha and numeric +-- @function input:set_allowerd_characters +-- @tparam string characters Regulax exp. for validate user input function M.set_allowed_characters(self, characters) self.allowed_characters = characters end +--- Reset current input selection and return previous value +-- @function input:reset_changes function M.reset_changes(self) self:set_text(self.previous_value) unselect(self) diff --git a/druid/druid.lua b/druid/druid.lua index 2bbcd43..8fe67ee 100644 --- a/druid/druid.lua +++ b/druid/druid.lua @@ -100,7 +100,6 @@ end -- Used to trigger on_focus_lost and on_focus_gain -- @function druid.on_window_callback -- @tparam string event Event param from window listener --- @tparam table data Data param from window listener function M.on_window_callback(event) local instances = get_druid_instances() @@ -130,7 +129,7 @@ end --- Callback on global language change event. --- Used to update all lang texts +-- Use to update all lang texts -- @function druid.on_language_change function M.on_language_change() local instances = get_druid_instances() diff --git a/druid/system/druid_instance.lua b/druid/system/druid_instance.lua index 5a5fb4a..5c774f6 100644 --- a/druid/system/druid_instance.lua +++ b/druid/system/druid_instance.lua @@ -235,6 +235,9 @@ function Druid.on_message(self, message_id, message, sender) end +--- Druid on focus lost interest function. +-- This one called by on_window_callback by global window listener +-- @function druid:on_focus_lost function Druid.on_focus_lost(self) local components = self.components[const.ON_FOCUS_LOST] if components then @@ -244,6 +247,10 @@ function Druid.on_focus_lost(self) end end + +--- Druid on focus gained interest function. +-- This one called by on_window_callback by global window listener +-- @function druid:on_focus_gained function Druid.on_focus_gained(self) local components = self.components[const.ON_FOCUS_GAINED] if components then @@ -254,6 +261,9 @@ function Druid.on_focus_gained(self) end +--- Druid on layout change function. +-- Called on update gui layout +-- @function druid:on_layout_change function Druid.on_layout_change(self) local components = self.components[const.ON_LAYOUT_CHANGE] if components then @@ -264,6 +274,10 @@ function Druid.on_layout_change(self) end +--- Druid on language change. +-- This one called by global gruid.on_language_change, but can be +-- call manualy to update all translations +-- @function druid.on_language_change function Druid.on_language_change(self) local components = self.components[const.ON_LANGUAGE_CHANGE] if components then From 97509ca30bdf1d7c261abf09fa134c5f62538c7b Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 12:10:00 +0300 Subject: [PATCH 13/17] Add marked_text support. Add input example page --- docs_md/changelog.md | 1 + druid/base/input.lua | 52 +- druid/const.lua | 20 +- druid/styles/default/style.lua | 2 + example/gui/main/main.gui | 999 +++++++++++++++++++++++++++++++ example/gui/main/main.gui_script | 3 + example/lang.lua | 2 + example/page/input.lua | 12 + game.project | 1 + input/game.input_binding | 2 +- 10 files changed, 1073 insertions(+), 21 deletions(-) create mode 100644 example/page/input.lua diff --git a/docs_md/changelog.md b/docs_md/changelog.md index a70a539..253fc99 100644 --- a/docs_md/changelog.md +++ b/docs_md/changelog.md @@ -14,6 +14,7 @@ Druid 0.3.0: - You can setup max length of the text - You can setup allowed characters. On add not allowed characters `on_input_wrong` will be called. By default it cause simple shake animation - The keyboard for input will not show on mobile HTML5 + - To make work different keyboard type, make sure value in game.project Android:InputMethod set to HidderInputField (https://defold.com/manuals/project-settings/#input-method) - Add button on_click_outside event. You can subscribe on this event in button. Was needed for Input component (click outside to deselect input field). - Add start_pos to button component diff --git a/druid/base/input.lua b/druid/base/input.lua index 81c8193..f98639b 100644 --- a/druid/base/input.lua +++ b/druid/base/input.lua @@ -11,12 +11,28 @@ local utf8 = require("druid.system.utf8") local M = 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.selected then self.previous_value = self.value self.selected = true + print("type", self.keyboard_type) gui.show_keyboard(self.keyboard_type, false) self.on_input_select:trigger(self:get_context()) @@ -43,7 +59,10 @@ end local function clear_and_select(self) - self:set_text("") + if self.style.IS_LONGTAP_ERASE then + self:set_text("") + end + select(self) end @@ -54,10 +73,10 @@ function M.init(self, click_node, text_node, keyboard_type) self.text = self.druid:new_text(text_node) self.selected = false - self.previous_value = "" - self.value = "" + self.value = self.text.last_value + self.previous_value = self.text.last_value + self.current_value = self.text.last_value self.marked_value = "" - self.current_value = "" self.is_empty = true self.text_width = 0 @@ -67,7 +86,7 @@ function M.init(self, click_node, text_node, keyboard_type) self.max_length = nil self.allowed_characters = nil - self.keyboard_type = keyboard_type or gui.KEYBOARD_TYPE_NUMBER_PAD + self.keyboard_type = keyboard_type or gui.KEYBOARD_TYPE_DEFAULT self.button = self.druid:new_button(click_node, select) self.button:set_style(self.style) @@ -116,8 +135,9 @@ function M.on_input(self, action_id, action) if action_id == const.ACTION_MARKED_TEXT then self.marked_value = action.text or "" if self.max_length then - input_text = utf8.sub(self.marked_value, 1, self.max_length) + self.marked_value = utf8.sub(self.marked_value, 1, self.max_length) end + print("marked text", self.marked_value) end if action_id == const.ACTION_BACKSPACE and (action.pressed or action.repeated) then @@ -139,7 +159,7 @@ function M.on_input(self, action_id, action) return true end - if input_text then + if input_text or #self.marked_value > 0 then self:set_text(input_text) return true end @@ -154,13 +174,22 @@ function M.on_focus_lost(self) end +function M.on_input_interrupt(self) + -- unselect(self) +end + + --- Set text for input field -- @function input:set_text -- @tparam string input_text The string to apply for input field function M.set_text(self, input_text) - self.value = 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 + -- Only update the text if it has changed + print("set text", self.value, ":::", self.marked_value) local current_value = self.value .. self.marked_value if current_value ~= self.current_value then @@ -169,8 +198,9 @@ function M.set_text(self, input_text) -- mask text if password field local masked_value, masked_marked_value if self.keyboard_type == gui.KEYBOARD_TYPE_PASSWORD then - masked_value = M.mask_text(self.text, "*") - masked_marked_value = M.mask_text(self.marked_text, "*") + 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 diff --git a/druid/const.lua b/druid/const.lua index 4af54b2..ae6d4ce 100644 --- a/druid/const.lua +++ b/druid/const.lua @@ -5,12 +5,14 @@ local M = {} M.ACTION_TEXT = hash("text") -M.ACTION_TOUCH = hash("touch") M.ACTION_MARKED_TEXT = hash("marked_text") + M.ACTION_BACKSPACE = hash("key_backspace") M.ACTION_ENTER = hash("key_enter") -M.ACTION_BACK = hash("back") +M.ACTION_BACK = hash("key_back") M.ACTION_ESC = hash("key_esc") + +M.ACTION_TOUCH = hash("touch") M.ACTION_SCROLL_UP = hash("scroll_up") M.ACTION_SCROLL_DOWN = hash("scroll_down") @@ -24,14 +26,14 @@ M.ALL = "all" --- Component Interests -M.ON_MESSAGE = hash("on_message") -M.ON_UPDATE = hash("on_update") -M.ON_INPUT_HIGH = hash("on_input_high") M.ON_INPUT = hash("on_input") -M.ON_LANGUAGE_CHANGE = hash("on_language_change") -M.ON_LAYOUT_CHANGE = hash("on_layout_change") +M.ON_UPDATE = hash("on_update") +M.ON_MESSAGE = hash("on_message") +M.ON_INPUT_HIGH = hash("on_input_high") M.ON_FOCUS_LOST = hash("on_focus_lost") M.ON_FOCUS_GAINED = hash("on_focus_gained") +M.ON_LAYOUT_CHANGE = hash("on_layout_change") +M.ON_LANGUAGE_CHANGE = hash("on_language_change") M.PIVOTS = { @@ -48,10 +50,10 @@ M.PIVOTS = { M.SPECIFIC_UI_MESSAGES = { - [M.ON_LANGUAGE_CHANGE] = "on_language_change", - [M.ON_LAYOUT_CHANGE] = "on_layout_change", [M.ON_FOCUS_LOST] = "on_focus_lost", [M.ON_FOCUS_GAINED] = "on_focus_gained", + [M.ON_LAYOUT_CHANGE] = "on_layout_change", + [M.ON_LANGUAGE_CHANGE] = "on_language_change", } diff --git a/druid/styles/default/style.lua b/druid/styles/default/style.lua index 7e40eb8..817a4d4 100644 --- a/druid/styles/default/style.lua +++ b/druid/styles/default/style.lua @@ -83,7 +83,9 @@ M["swipe"] = { M["input"] = { + IS_LONGTAP_ERASE = true, BUTTON_SELECT_INCREASE = 1.1, + MASK_DEFAULT_CHAR = "*", on_select = function(self, button_node) local target_scale = self.button.start_scale diff --git a/example/gui/main/main.gui b/example/gui/main/main.gui index c46c188..e5e02f7 100644 --- a/example/gui/main/main.gui +++ b/example/gui/main/main.gui @@ -7850,6 +7850,1005 @@ nodes { text_leading: 1.0 text_tracking: 0.0 } +nodes { + position { + x: 3000.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 1.0 + y: 1.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "kenney/empty" + id: "input_page" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_STRETCH + parent: "C_Anchor" + layer: "image" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_AUTO +} +nodes { + position { + x: 0.0 + y: 200.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 1.0 + y: 1.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "kenney/empty" + id: "input_usual" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "input_page" + layer: "image" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_AUTO +} +nodes { + position { + x: -250.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.7 + y: 0.7 + z: 1.0 + w: 1.0 + } + size { + x: 300.0 + y: 60.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Usual input:" + font: "game" + id: "input_usual_header" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_W + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "input_usual" + layer: "text" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 0.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 130.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 190.0 + y: 45.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "kenney/progress_back" + id: "input_box_usual" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "input_usual" + layer: "image" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_AUTO +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.7 + y: 0.7 + z: 1.0 + w: 1.0 + } + size { + x: 250.0 + y: 50.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Initial text" + font: "game" + id: "input_text_usual" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: true + parent: "input_box_usual" + layer: "text" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 0.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 0.0 + y: 100.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 1.0 + y: 1.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "kenney/empty" + id: "input_password" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "input_page" + layer: "image" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_AUTO +} +nodes { + position { + x: -250.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.7 + y: 0.7 + z: 1.0 + w: 1.0 + } + size { + x: 300.0 + y: 60.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Password:" + font: "game" + id: "input_password_header" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_W + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "input_password" + layer: "text" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 0.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 130.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 190.0 + y: 45.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "kenney/progress_back" + id: "input_box_password" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "input_password" + layer: "image" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_AUTO +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.7 + y: 0.7 + z: 1.0 + w: 1.0 + } + size { + x: 250.0 + y: 50.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "" + font: "game" + id: "input_text_password" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: true + parent: "input_box_password" + layer: "text" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 0.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 1.0 + y: 1.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "kenney/empty" + id: "input_email" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "input_page" + layer: "image" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_AUTO +} +nodes { + position { + x: -250.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.7 + y: 0.7 + z: 1.0 + w: 1.0 + } + size { + x: 300.0 + y: 60.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Email:" + font: "game" + id: "input_email_header" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_W + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "input_email" + layer: "text" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 0.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 130.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 190.0 + y: 45.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "kenney/progress_back" + id: "input_box_email" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "input_email" + layer: "image" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_AUTO +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.7 + y: 0.7 + z: 1.0 + w: 1.0 + } + size { + x: 250.0 + y: 50.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "" + font: "game" + id: "input_text_email" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: true + parent: "input_box_email" + layer: "text" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 0.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 0.0 + y: -100.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 1.0 + y: 1.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "kenney/empty" + id: "input_numpad" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "input_page" + layer: "image" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_AUTO +} +nodes { + position { + x: -250.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.7 + y: 0.7 + z: 1.0 + w: 1.0 + } + size { + x: 300.0 + y: 60.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Numpad:" + font: "game" + id: "input_numbad_header" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_W + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "input_numpad" + layer: "text" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 0.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 130.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 190.0 + y: 45.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "kenney/progress_back" + id: "input_box_numpad" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "input_numpad" + layer: "image" + inherit_alpha: true + slice9 { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: false + size_mode: SIZE_MODE_AUTO +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.7 + y: 0.7 + z: 1.0 + w: 1.0 + } + size { + x: 250.0 + y: 50.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "" + font: "game" + id: "input_text_numpad" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: true + parent: "input_box_numpad" + layer: "text" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 0.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} nodes { position { x: 0.0 diff --git a/example/gui/main/main.gui_script b/example/gui/main/main.gui_script index 275561f..f38a0ae 100644 --- a/example/gui/main/main.gui_script +++ b/example/gui/main/main.gui_script @@ -8,6 +8,7 @@ local text_page = require("example.page.texts") local button_page = require("example.page.button") local scroll_page = require("example.page.scroll") local slider_page = require("example.page.slider") +local input_page = require("example.page.input") local pages = { "main_page", @@ -15,6 +16,7 @@ local pages = { "button_page", "scroll_page", "slider_page", + "input_page", } local function on_control_button(self, delta) @@ -70,6 +72,7 @@ function init(self) button_page.setup_page(self) scroll_page.setup_page(self) slider_page.setup_page(self) + input_page.setup_page(self) -- Refresh state on_control_button(self, 0) diff --git a/example/lang.lua b/example/lang.lua index 890a64d..f0c15e1 100644 --- a/example/lang.lua +++ b/example/lang.lua @@ -8,6 +8,7 @@ local en = { button_page = "Button page", scroll_page = "Scroll page", slider_page = "Slider page", + input_page = "Input page", ui_section_button = "Button", ui_section_text = "Text", ui_section_timer = "Timer", @@ -25,6 +26,7 @@ local ru = { button_page = "Кнопки", scroll_page = "Скролл", slider_page = "Слайдеры", + input_page = "Текст. ввод", ui_section_button = "Кнопка", ui_section_text = "Текст", ui_section_timer = "Таймер", diff --git a/example/page/input.lua b/example/page/input.lua new file mode 100644 index 0000000..7890d31 --- /dev/null +++ b/example/page/input.lua @@ -0,0 +1,12 @@ +local M = {} + + +function M.setup_page(self) + self.druid:new_input("input_box_usual", "input_text_usual") + self.druid:new_input("input_box_password", "input_text_password", gui.KEYBOARD_TYPE_PASSWORD) + self.druid:new_input("input_box_email", "input_text_email", gui.KEYBOARD_TYPE_EMAIL) + self.druid:new_input("input_box_numpad", "input_text_numpad", gui.KEYBOARD_TYPE_NUMBER_PAD) +end + + +return M diff --git a/game.project b/game.project index 0d0991d..5e17c32 100644 --- a/game.project +++ b/game.project @@ -38,4 +38,5 @@ texture_profiles = /example/custom.texture_profiles [android] package = com.insality.druid +input_method = HiddenInputField diff --git a/input/game.input_binding b/input/game.input_binding index 1c965b4..b873ebd 100644 --- a/input/game.input_binding +++ b/input/game.input_binding @@ -4,7 +4,7 @@ key_trigger { } key_trigger { input: KEY_BACK - action: "back" + action: "key_back" } key_trigger { input: KEY_SPACE From 67038b5c77274e6f16c1409ef99ff5061b78ea8c Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 13:44:23 +0300 Subject: [PATCH 14/17] Add increase/reset input priority. Input field now increase self prioirty --- druid/base/button.lua | 4 ++++ druid/base/input.lua | 14 +++++++++++--- druid/component.lua | 14 ++++++++++++++ druid/system/druid_instance.lua | 25 ++++++++++++++++++++----- example/gui/main/main.gui | 12 ++++++------ example/page/input.lua | 1 + 6 files changed, 56 insertions(+), 14 deletions(-) diff --git a/druid/base/button.lua b/druid/base/button.lua index 9d6bc32..362cd1e 100644 --- a/druid/base/button.lua +++ b/druid/base/button.lua @@ -270,17 +270,21 @@ end -- no click events outside stencil node -- @function button:set_click_zone -- @tparam node zone Gui node +-- @tparam druid.button Self instance to make chain calls function M.set_click_zone(self, zone) self.click_zone = self:get_node(zone) self.hover:set_click_zone(zone) + return self end --- Set key-code to trigger this button -- @function button:set_key_trigger -- @tparam hash key The action_id of the key +-- @tparam druid.button Self instance to make chain calls function M.set_key_trigger(self, key) self.key_trigger = hash(key) + return self end diff --git a/druid/base/input.lua b/druid/base/input.lua index f98639b..7564537 100644 --- a/druid/base/input.lua +++ b/druid/base/input.lua @@ -30,9 +30,11 @@ local function select(self) gui.reset_keyboard() self.marked_value = "" if not self.selected then + self:increase_input_priority() + self.button:increase_input_priority() self.previous_value = self.value self.selected = true - print("type", self.keyboard_type) + gui.show_keyboard(self.keyboard_type, false) self.on_input_select:trigger(self:get_context()) @@ -47,7 +49,10 @@ local function unselect(self) gui.reset_keyboard() self.marked_value = "" if self.selected then + self:reset_input_priority() + self.button:reset_input_priority() self.selected = false + gui.hide_keyboard() self.on_input_unselect:trigger(self:get_context()) @@ -137,7 +142,6 @@ function M.on_input(self, action_id, action) if self.max_length then self.marked_value = utf8.sub(self.marked_value, 1, self.max_length) end - print("marked text", self.marked_value) end if action_id == const.ACTION_BACKSPACE and (action.pressed or action.repeated) then @@ -189,7 +193,6 @@ function M.set_text(self, input_text) end -- Only update the text if it has changed - print("set text", self.value, ":::", self.marked_value) local current_value = self.value .. self.marked_value if current_value ~= self.current_value then @@ -239,17 +242,22 @@ end -- Pass nil to make input field unliminted (by default) -- @function input:set_max_length -- @tparam number max_length Maximum length for input text field +-- @tparam druid.input Self instance to make chain calls function M.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 -- @function input:set_allowerd_characters -- @tparam string characters Regulax exp. for validate user input +-- @tparam druid.input Self instance to make chain calls function M.set_allowed_characters(self, characters) self.allowed_characters = characters + return self end diff --git a/druid/component.lua b/druid/component.lua index 47486b4..9b79b88 100644 --- a/druid/component.lua +++ b/druid/component.lua @@ -86,6 +86,19 @@ function Component.get_interests(self) end +--- Increase input priority in current input stack +-- @function component:increase_input_priority +function Component.increase_input_priority(self) + self._meta.increased_input_priority = true +end + +--- Reset input priority in current input stack +-- @function component:reset_input_priority +function Component.reset_input_priority(self) + self._meta.increased_input_priority = false +end + + --- Get node for component by name. -- If component has nodes, node_or_name should be string -- It auto pick node by template name or from nodes by clone_tree @@ -135,6 +148,7 @@ function Component.setup_component(self, context, style) context = nil, nodes = nil, style = nil, + increased_input_priority = false } self:set_context(context) diff --git a/druid/system/druid_instance.lua b/druid/system/druid_instance.lua index 5c774f6..9c04b33 100644 --- a/druid/system/druid_instance.lua +++ b/druid/system/druid_instance.lua @@ -90,12 +90,27 @@ local function process_input(action_id, action, components, is_input_consumed) for i = #components, 1, -1 do local component = components[i] + -- Process increased input priority first + if component._meta.increased_input_priority then + if not is_input_consumed then + is_input_consumed = component:on_input(action_id, action) + else + if component.on_input_interrupt then + component:on_input_interrupt() + end + end + end + end - if not is_input_consumed then - is_input_consumed = component:on_input(action_id, action) - else - if component.on_input_interrupt then - component:on_input_interrupt() + for i = #components, 1, -1 do + local component = components[i] + if not component._meta.increased_input_priority then + if not is_input_consumed then + is_input_consumed = component:on_input(action_id, action) + else + if component.on_input_interrupt then + component:on_input_interrupt() + end end end end diff --git a/example/gui/main/main.gui b/example/gui/main/main.gui index e5e02f7..f1883f8 100644 --- a/example/gui/main/main.gui +++ b/example/gui/main/main.gui @@ -3183,7 +3183,7 @@ nodes { } size { x: 250.0 - y: 80.0 + y: 50.0 z: 0.0 w: 1.0 } @@ -3214,7 +3214,7 @@ nodes { w: 1.0 } adjust_mode: ADJUST_MODE_FIT - line_break: true + line_break: false parent: "input_box" layer: "" inherit_alpha: true @@ -8130,7 +8130,7 @@ nodes { w: 1.0 } adjust_mode: ADJUST_MODE_FIT - line_break: true + line_break: false parent: "input_box_usual" layer: "text" inherit_alpha: true @@ -8366,7 +8366,7 @@ nodes { w: 1.0 } adjust_mode: ADJUST_MODE_FIT - line_break: true + line_break: false parent: "input_box_password" layer: "text" inherit_alpha: true @@ -8602,7 +8602,7 @@ nodes { w: 1.0 } adjust_mode: ADJUST_MODE_FIT - line_break: true + line_break: false parent: "input_box_email" layer: "text" inherit_alpha: true @@ -8838,7 +8838,7 @@ nodes { w: 1.0 } adjust_mode: ADJUST_MODE_FIT - line_break: true + line_break: false parent: "input_box_numpad" layer: "text" inherit_alpha: true diff --git a/example/page/input.lua b/example/page/input.lua index 7890d31..be5d6fd 100644 --- a/example/page/input.lua +++ b/example/page/input.lua @@ -6,6 +6,7 @@ function M.setup_page(self) self.druid:new_input("input_box_password", "input_text_password", gui.KEYBOARD_TYPE_PASSWORD) self.druid:new_input("input_box_email", "input_text_email", gui.KEYBOARD_TYPE_EMAIL) self.druid:new_input("input_box_numpad", "input_text_numpad", gui.KEYBOARD_TYPE_NUMBER_PAD) + :set_allowed_characters("[%d,.]") end From 866e8727fcc1f2b83a694485d88845a73f37714c Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 14:17:25 +0300 Subject: [PATCH 15/17] Correct events and changelog --- docs_md/changelog.md | 29 +++++++++++++++++++---------- druid/base/input.lua | 6 +++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/docs_md/changelog.md b/docs_md/changelog.md index 253fc99..284a8e2 100644 --- a/docs_md/changelog.md +++ b/docs_md/changelog.md @@ -1,19 +1,20 @@ Druid 0.3.0: -- Druid:final now is important function for correct working +- `Druid:final` now is important function for correct working -- Add swipe basic component +- Add _swipe_ basic component - Swipe component handle simple swipe gestures on node. It has single callback with direction on swipe. You can adjust a several parameters of swipe in druid style. + - Swipe can be triggered on action.released or while user is make swiping (in process) - Add swipe example at main Druid example. Try swipe left/right to switch example pages. -- Add input basic component - - Input component handle user text input. Input contains from button and text component. Button needed for selecting input field - - Long click on input field for clear and select input field +- Add _input_ basic component + - Input component handle user text input. Input contains from button and text components. Button needed for selecting/unselecting input field + - Long click on input field for clear and select input field (clearing can be disable via styles) - Click outside of button to unselect input field - On focus lost (game minimized) input field will be unselected - You can setup max length of the text - You can setup allowed characters. On add not allowed characters `on_input_wrong` will be called. By default it cause simple shake animation - - The keyboard for input will not show on mobile HTML5 + - The keyboard for input will not show on mobile HTML5. So input field in mobile HTML5 is not working now - To make work different keyboard type, make sure value in game.project Android:InputMethod set to HidderInputField (https://defold.com/manuals/project-settings/#input-method) - Add button on_click_outside event. You can subscribe on this event in button. Was needed for Input component (click outside to deselect input field). @@ -21,11 +22,19 @@ Druid 0.3.0: - Changed input binding settings. Add backspace, enter, text and marked_text. Backspace now is different from android back button. -- Changed component interest: Renamed on_change_language -> on_language_change -- Add two new component interests: on_focus_gain and on_focus_lost +- Renamed on_change_language -> on_language_change component interest + +- Add basic component two functions: `increase_input_priority` and `reset_input_priority`. It used to process component input first in current input stack (there is two input stacks: INPUT and INPUT_HIGH). Example: on selecting input field, it increase input self priority until it be unselected + +- Add two new component interests: `on_focus_gain` and `on_focus_lost` + - Add global druid events: - on_window_callback: call `druid.on_window_callback(event)` for on_focus_gain/lost correct work - - on_language_change: call `druid.on_language_change()` for update all druid instances lang components - - on_layout_change: call `druid.on_layout_change()` for update all gui layouts (unsupported now) + - on_language_change: call `druid.on_language_change()` (#38) for update all druid instances lang components + - on_layout_change: call `druid.on_layout_change()` (#37) for update all gui layouts (unimplemented now) - Add several examples to druid-assets respository + +- Known issues: + - Adjusting text size by height works wrong. Adjusting single line texting works fine + \ No newline at end of file diff --git a/druid/base/input.lua b/druid/base/input.lua index 7564537..f0c5e66 100644 --- a/druid/base/input.lua +++ b/druid/base/input.lua @@ -211,14 +211,14 @@ function M.set_text(self, input_text) 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 - local final_text = value .. marked_value - self.text:set_to(final_text) - 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) From 668ecff090a1ed52e405feda142bada0e3a119cf Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 14:39:12 +0300 Subject: [PATCH 16/17] Update documentation --- README.md | 27 +++++++++++++++++---- docs_md/01-components.md | 9 +++++++ docs_md/02-creating_custom_components.md | 19 +++++++++++---- docs_md/03-styles.md | 4 ---- druid/base/input.lua | 29 +++++++++++++++++++++++ druid/druid.lua | 1 + media/input_binding.png | Bin 22818 -> 0 bytes media/input_binding_1.png | Bin 0 -> 16657 bytes media/input_binding_2.png | Bin 0 -> 27280 bytes 9 files changed, 77 insertions(+), 12 deletions(-) delete mode 100644 media/input_binding.png create mode 100644 media/input_binding_1.png create mode 100644 media/input_binding_2.png diff --git a/README.md b/README.md index 8fc8c8b..d3ba89c 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,13 @@ Or point to the ZIP file of a [specific release](https://github.com/Insality/dr For **Druid** to work requires next input bindings: - Mouse trigger - `Button 1` -> `touch` _For basic input components_ -- Key trigger - `Backspace` -> `back` _For back_handler component_ -- Key trigger - `Back` -> `back` _For back_handler component, Android back button_ +- Key trigger - `Backspace` -> `key_backspace` _For back_handler component, input component_ +- Key trigger - `Back` -> `key_back` _For back_handler component, Android back button, input component_ +- Key trigger - `Enter` -> `key_enter` _For input component, optional_ +- Key trigger - `Esc` -> `key_esc` _For input component, optional_ -![](media/input_binding.png) +![](media/input_binding_2.png) +![](media/input_binding_1.png) ### Input capturing [optional] @@ -55,6 +58,18 @@ druid.set_text_function(callback) -- Used for change default druid style druid.set_default_style(your_style) + +-- Call this function on language changing in the game, +-- to retranslate all lang_text components: +druid.on_languge_change() + +-- Call this function on layout changing in the game, +-- to reapply layouts +druid.on_layout_change() + +-- Call this function inside window.set_listener +-- to catch game focus lost/gained callbacks: +druid.on_window_callback(event) ``` @@ -112,11 +127,15 @@ local function button_callback(self) print("Button was clicked!") end -local function init(self) +function init(self) self.druid = druid.new(self) self.druid:new_button("button_node_name", button_callback) end +function final(self) + self.druid:final() +end + function on_input(self, action_id, action) return self.druid:on_input(action_id, action) end diff --git a/docs_md/01-components.md b/docs_md/01-components.md index e4c79b5..fd94779 100644 --- a/docs_md/01-components.md +++ b/docs_md/01-components.md @@ -164,8 +164,17 @@ Pin node (node_name in params) should be placed in zero position (initial). It w Basic Druid text input component (unimplemented) ### Setup +Create input component with druid: `input = druid:new_input(button_node_name, text_node_name, keyboard_type)` ### Notes +- Input component handle user text input. Input contains from button and text components. Button needed for selecting/unselecting input field +- Long click on input field for clear and select input field (clearing can be disable via styles) +- Click outside of button to unselect input field +- On focus lost (game minimized) input field will be unselected +- You can setup max length of the text +- You can setup allowed characters. On add not allowed characters `on_input_wrong` will be called. By default it cause simple shake animation +- The keyboard for input will not show on mobile HTML5. So input field in mobile HTML5 is not working now +- To make work different keyboard type, make sure value in game.project Android:InputMethod set to HidderInputField (https://defold.com/manuals/project-settings/#input-method) ## Checkbox diff --git a/docs_md/02-creating_custom_components.md b/docs_md/02-creating_custom_components.md index f163c5c..38534e3 100644 --- a/docs_md/02-creating_custom_components.md +++ b/docs_md/02-creating_custom_components.md @@ -32,8 +32,8 @@ end function M.on_message(self, message_id, message, sender) end --- Call only if component with ON_CHANGE_LANGUAGE interest -function M.on_change_language(self) +-- Call only if component with ON_LANGUAGE_CHANGE interest +function M.on_language_change(self) end -- Call only if component with ON_LAYOUT_CHANGE interest @@ -45,6 +45,14 @@ end function M.on_input_interrupt(self) end +-- Call, if game lost focus. Need ON_FOCUS_LOST intereset +function M.on_focus_lost(self) +end + +-- Call, if game gained focus. Need ON_FOCUS_GAINED intereset +function M.on_focus_gained(self) +end + -- Call on component remove or on druid:final function M.on_remove(self) end @@ -92,10 +100,13 @@ There is next interests in druid: - **ON_INPUT** - component will receive input from on_input, after other components with ON_INPUT_HIGH -- **ON_CHANGE_LANGUAGE** - will call _on_change_language_ function on language change trigger +- **ON_LANGUAGE_CHANGE** - will call _on_language_change_ function on language change trigger -- **ON_LAYOUT_CHANGED** will call _on_layout_change_ function on layout change trigger +- **ON_LAYOUT_CHANGE** will call _on_layout_change_ function on layout change trigger +- **ON_FOCUS_LOST** will call _on_focust_lost_ function in on focus lost event. You need to pass window_callback to global `druid:on_window_callback` + +- **ON_FOCUS_GAINED** will call _on_focust_gained_ function in on focus gained event. You need to pass window_callback to global `druid:on_window_callback` ## Best practice on custom components On each component recommended describe component scheme in next way: diff --git a/docs_md/03-styles.md b/docs_md/03-styles.md index a018c43..d651333 100644 --- a/docs_md/03-styles.md +++ b/docs_md/03-styles.md @@ -41,7 +41,3 @@ local function init(self) self.button:set_style(my_style) end ``` - -## Create custom components -Styles is just lua table, so it can be described in just one single file -__TODO__ diff --git a/druid/base/input.lua b/druid/base/input.lua index f0c5e66..b4299bf 100644 --- a/druid/base/input.lua +++ b/druid/base/input.lua @@ -3,6 +3,35 @@ -- @author Part of code from Britzl gooey input component -- @module druid.input +--- Component events +-- @table Events +-- @tfield druid_event on_input_select (self, button_node) On input field select callback +-- @tfield druid_event on_input_unselect (self, button_node) On input field unselect callback +-- @tfield druid_event on_input_text (self, input_text) On input field text change callback +-- @tfield druid_event on_input_empty (self, input_text) On input field text change to empty string callback +-- @tfield druid_event on_input_full (self, input_text) On input field text change to max length string callback +-- @tfield druid_event on_input_wrong (self, params, button_instance) On trying user input with not allowed character callback + +--- Component fields +-- @table Fields +-- @tfield druid.text text Text component +-- @tfield druid.button button Button component +-- @tfield bool is_selected Is current input selected now +-- @tfield bool is_empty Is current input is empty now +-- @tfield[opt] number max_length Max length for input text +-- @tfield[opt] string allowerd_characters Pattern matching for user input +-- @tfield number keyboard_type Gui keyboard type for input field + +--- Component style params +-- @table Style +-- @tfield bool IS_LONGTAP_ERASE Is long tap will erase current input data +-- @tfield number BUTTON_SELECT_INCREASE Button scale multiplier on selecting input field +-- @tfield 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 Custom button style for input node + local Event = require("druid.event") local const = require("druid.const") local component = require("druid.component") diff --git a/druid/druid.lua b/druid/druid.lua index 8fe67ee..eb10f44 100644 --- a/druid/druid.lua +++ b/druid/druid.lua @@ -139,4 +139,5 @@ function M.on_language_change() end end + return M diff --git a/media/input_binding.png b/media/input_binding.png deleted file mode 100644 index f5b68d6cdacbec2a4b74623859fd2e35a5908e90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22818 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 literal 0 HcmV?d00001 diff --git a/media/input_binding_2.png b/media/input_binding_2.png new file mode 100644 index 0000000000000000000000000000000000000000..26b6f153bed28595b13142023919777e3179e1d1 GIT binary patch literal 27280 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 literal 0 HcmV?d00001 From dd2627c4d26b28a69d9d770bd6af486e79521e1e Mon Sep 17 00:00:00 2001 From: Insality Date: Sat, 18 Apr 2020 14:39:27 +0300 Subject: [PATCH 17/17] Update ldoc --- docs/index.html | 7 +- docs/modules/component.html | 39 ++- docs/modules/druid.back_handler.html | 3 +- docs/modules/druid.blocker.html | 3 +- docs/modules/druid.button.html | 33 +- docs/modules/druid.checkbox.html | 3 +- docs/modules/druid.checkbox_group.html | 3 +- docs/modules/druid.grid.html | 3 +- docs/modules/druid.helper.html | 3 +- docs/modules/druid.hover.html | 3 +- docs/modules/druid.html | 146 ++++++++- docs/modules/druid.input.html | 302 +++++++++++++++++- docs/modules/druid.lang_text.html | 3 +- docs/modules/druid.progress.html | 3 +- docs/modules/druid.radio_group.html | 3 +- docs/modules/druid.scroll.html | 3 +- docs/modules/druid.slider.html | 3 +- docs/modules/druid.swipe.html | 33 +- docs/modules/druid.text.html | 55 +++- docs/modules/druid.timer.html | 3 +- docs/modules/druid_event.html | 3 +- docs/modules/druid_instance.html | 80 ++++- docs/topics/01-components.md.html | 12 +- .../02-creating_custom_components.md.html | 22 +- docs/topics/03-styles.md.html | 11 +- docs/topics/04-druid_assets.md.html | 3 +- docs/topics/05-examples.md.html | 3 +- docs/topics/README.md.html | 30 +- docs/topics/changelog.md.html | 132 ++++++++ 29 files changed, 894 insertions(+), 56 deletions(-) create mode 100644 docs/topics/changelog.md.html diff --git a/docs/index.html b/docs/index.html index 639dc54..3ffa283 100644 --- a/docs/index.html +++ b/docs/index.html @@ -60,6 +60,7 @@
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -180,6 +181,10 @@ 05-examples.md + + changelog.md + + README.md @@ -190,7 +195,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/component.html b/docs/modules/component.html index 1748f3a..37fd89c 100644 --- a/docs/modules/component.html +++ b/docs/modules/component.html @@ -67,6 +67,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -118,6 +119,14 @@ Get current component interests + increase_input_priority() + Increase input priority in current input stack + + + reset_input_priority() + Reset input priority in current input stack + + get_node(node_or_name) Get node for component by name. @@ -325,6 +334,34 @@ + +
    + + increase_input_priority() +
    +
    + Increase input priority in current input stack + + + + + + + +
    +
    + + reset_input_priority() +
    +
    + Reset input priority in current input stack + + + + + + +
    @@ -441,7 +478,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.back_handler.html b/docs/modules/druid.back_handler.html index dae7f4a..0bc2cc0 100644 --- a/docs/modules/druid.back_handler.html +++ b/docs/modules/druid.back_handler.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -216,7 +217,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.blocker.html b/docs/modules/druid.blocker.html index 4f6e257..02feafd 100644 --- a/docs/modules/druid.blocker.html +++ b/docs/modules/druid.blocker.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -235,7 +236,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.button.html b/docs/modules/druid.button.html index 9865e31..b59dd1c 100644 --- a/docs/modules/druid.button.html +++ b/docs/modules/druid.button.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -97,11 +98,11 @@ Return button enabled state - set_click_zone(zone) + set_click_zone(zone, Self) Strict button click area. - set_key_trigger(key) + set_key_trigger(key, Self) Set key-code to trigger this button @@ -210,7 +211,7 @@
    - set_click_zone(zone) + set_click_zone(zone, Self)
    Strict button click area. Useful for @@ -223,6 +224,10 @@ node Gui node +
  • Self + druid.button + instance to make chain calls +
  • @@ -232,7 +237,7 @@
    - set_key_trigger(key) + set_key_trigger(key, Self)
    Set key-code to trigger this button @@ -244,6 +249,10 @@ hash The action_id of the key +
  • Self + druid.button + instance to make chain calls +
  • @@ -305,6 +314,10 @@ druid_event (self, params, buttoninstance, time) On button hold before longclick callback +
  • on_click_outside + druid_event + (self, params, button_instance) On click outside of button +
  • @@ -335,6 +348,10 @@ vector3 Initial scale of anim_node +
  • start_pos + vector3 + Initial pos of anim_node +
  • pos vector3 Initial pos of anim_node @@ -384,12 +401,6 @@
  • on_set_enabled function (self, node, enabled_state) -
  • -
  • IS_HOVER - bool - - -
  • @@ -405,7 +416,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.checkbox.html b/docs/modules/druid.checkbox.html index 7081605..1411d1c 100644 --- a/docs/modules/druid.checkbox.html +++ b/docs/modules/druid.checkbox.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -278,7 +279,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.checkbox_group.html b/docs/modules/druid.checkbox_group.html index df09ed2..0a7c625 100644 --- a/docs/modules/druid.checkbox_group.html +++ b/docs/modules/druid.checkbox_group.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -240,7 +241,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.grid.html b/docs/modules/druid.grid.html index f6c4c58..0a0ae13 100644 --- a/docs/modules/druid.grid.html +++ b/docs/modules/druid.grid.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -371,7 +372,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.helper.html b/docs/modules/druid.helper.html index f4c3af0..822ba15 100644 --- a/docs/modules/druid.helper.html +++ b/docs/modules/druid.helper.html @@ -67,6 +67,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -237,7 +238,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.hover.html b/docs/modules/druid.hover.html index 3135adf..5313e41 100644 --- a/docs/modules/druid.hover.html +++ b/docs/modules/druid.hover.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -212,7 +213,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.html b/docs/modules/druid.html index 076a530..021f3db 100644 --- a/docs/modules/druid.html +++ b/docs/modules/druid.html @@ -67,6 +67,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -107,6 +108,30 @@ new(context[, style]) Create Druid instance. + + set_default_style(style) + Set new default style. + + + set_text_function(callback) + Set text function. + + + set_sound_function(callback) + Set sound function. + + + on_window_callback(event) + Callback on global window event. + + + on_layout_change() + Callback on global layout change event. + + + on_language_change() + Callback on global language change event. +
    @@ -174,6 +199,125 @@ +
    +
    + + set_default_style(style) +
    +
    + Set new default style. + + +

    Parameters:

    +
      +
    • style + table + Druid style module +
    • +
    + + + + + +
    +
    + + set_text_function(callback) +
    +
    + Set text function. + Druid locale component will call this function + to get translated text. After settextfuntion + all existing locale component will be updated + + +

    Parameters:

    +
      +
    • callback + function + Get localized text function +
    • +
    + + + + + +
    +
    + + set_sound_function(callback) +
    +
    + Set sound function. + Component will call this function to + play sound by sound_id + + +

    Parameters:

    +
      +
    • callback + function + Sound play callback +
    • +
    + + + + + +
    +
    + + on_window_callback(event) +
    +
    + Callback on global window event. + Used to trigger onfocuslost and onfocusgain + + +

    Parameters:

    +
      +
    • event + string + Event param from window listener +
    • +
    + + + + + +
    +
    + + on_layout_change() +
    +
    + Callback on global layout change event. + + + + + + + +
    +
    + + on_language_change() +
    +
    + Callback on global language change event. + Use to update all lang texts + + + + + + +
    @@ -182,7 +326,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.input.html b/docs/modules/druid.input.html index e603bae..8f6b84b 100644 --- a/docs/modules/druid.input.html +++ b/docs/modules/druid.input.html @@ -30,6 +30,11 @@
  • Index
  • +

    Contents

    +

    Modules

    @@ -63,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -72,22 +78,312 @@

    Module druid.input

    Druid input text component.

    -

    Carry on user text input - UNIMPLEMENTED

    +

    Carry on user text input

    +

    Info:

    +
      +
    • Author: Part of code from Britzl gooey input component
    • +
    +

    Functions

    + + + + + + + + + + + + + + + + + + + + + +
    set_text(input_text)Set text for input field
    get_text()Return current input field text
    set_max_length(max_length, Self)Set maximum length for input field.
    set_allowerd_characters(characters, Self)Set allowed charaters for input field.
    reset_changes()Reset current input selection and return previous value
    +

    Tables

    + + + + + + + + + + + + + +
    EventsComponent events
    FieldsComponent fields
    StyleComponent style params


    +

    Functions

    + +
    +
    + + set_text(input_text) +
    +
    + Set text for input field + + +

    Parameters:

    +
      +
    • input_text + string + The string to apply for input field +
    • +
    + + + + + +
    +
    + + get_text() +
    +
    + Return current input field text + + + +

    Returns:

    +
      + + string + The current input field text +
    + + + + +
    +
    + + set_max_length(max_length, Self) +
    +
    + Set maximum length for input field. + Pass nil to make input field unliminted (by default) + + +

    Parameters:

    +
      +
    • max_length + number + Maximum length for input text field +
    • +
    • Self + druid.input + instance to make chain calls +
    • +
    + + + + + +
    +
    + + set_allowerd_characters(characters, Self) +
    +
    + Set allowed charaters for input field. + See: https://defold.com/ref/stable/string/ + ex: [%a%d] for alpha and numeric + + +

    Parameters:

    +
      +
    • characters + string + Regulax exp. for validate user input +
    • +
    • Self + druid.input + instance to make chain calls +
    • +
    + + + + + +
    +
    + + reset_changes() +
    +
    + Reset current input selection and return previous value + + + + + + + +
    +
    +

    Tables

    + +
    +
    + + Events +
    +
    + Component events + + +

    Fields:

    +
      +
    • on_input_select + druid_event + (self, button_node) On input field select callback +
    • +
    • on_input_unselect + druid_event + (self, button_node) On input field unselect callback +
    • +
    • on_input_text + druid_event + (self, input_text) On input field text change callback +
    • +
    • on_input_empty + druid_event + (self, input_text) On input field text change to empty string callback +
    • +
    • on_input_full + druid_event + (self, input_text) On input field text change to max length string callback +
    • +
    • on_input_wrong + druid_event + (self, params, button_instance) On trying user input with not allowed character callback +
    • +
    + + + + + +
    +
    + + Fields +
    +
    + Component fields + + +

    Fields:

    +
      +
    • text + druid.text + Text component +
    • +
    • button + druid.button + Button component +
    • +
    • is_selected + bool + Is current input selected now +
    • +
    • is_empty + bool + Is current input is empty now +
    • +
    • max_length + number + Max length for input text + (optional) +
    • +
    • allowerd_characters + string + Pattern matching for user input + (optional) +
    • +
    • keyboard_type + number + Gui keyboard type for input field +
    • +
    + + + + + +
    +
    + + Style +
    +
    + Component style params + + +

    Fields:

    +
      +
    • IS_LONGTAP_ERASE + bool + Is long tap will erase current input data +
    • +
    • BUTTON_SELECT_INCREASE + number + Button scale multiplier on selecting input field +
    • +
    • MASK_DEFAULT_CHAR + string + Default character mask for password input +
    • +
    • on_select + function + (self, button_node) Callback on input field selecting +
    • +
    • on_unselect + function + (self, button_node) Callback on input field unselecting +
    • +
    • on_input_wrong + function + (self, button_node) Callback on wrong user input +
    • +
    • button + table + Custom button style for input node +
    • +
    + + + + + +
    +
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.lang_text.html b/docs/modules/druid.lang_text.html index 31b46a3..3ffaa4c 100644 --- a/docs/modules/druid.lang_text.html +++ b/docs/modules/druid.lang_text.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -241,7 +242,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.progress.html b/docs/modules/druid.progress.html index f6d2790..2181941 100644 --- a/docs/modules/druid.progress.html +++ b/docs/modules/druid.progress.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -380,7 +381,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.radio_group.html b/docs/modules/druid.radio_group.html index 91cb3f1..e2de393 100644 --- a/docs/modules/druid.radio_group.html +++ b/docs/modules/druid.radio_group.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -240,7 +241,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.scroll.html b/docs/modules/druid.scroll.html index c35e602..a4b9263 100644 --- a/docs/modules/druid.scroll.html +++ b/docs/modules/druid.scroll.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -508,7 +509,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.slider.html b/docs/modules/druid.slider.html index 9ffc418..2c42e44 100644 --- a/docs/modules/druid.slider.html +++ b/docs/modules/druid.slider.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -279,7 +280,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.swipe.html b/docs/modules/druid.swipe.html index 212bbc9..15505fb 100644 --- a/docs/modules/druid.swipe.html +++ b/docs/modules/druid.swipe.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -94,6 +95,10 @@

    Tables

    + + + + @@ -162,6 +167,32 @@

    Tables

    +
    + + Fields +
    +
    + Components fields + + +

    Fields:

    +
      +
    • node + node + Swipe node +
    • +
    • click_zone + node + Restriction zone + (optional) +
    • +
    + + + + + +
    Events @@ -219,7 +250,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.text.html b/docs/modules/druid.text.html index 74b2de3..15e8ccc 100644 --- a/docs/modules/druid.text.html +++ b/docs/modules/druid.text.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -88,6 +89,10 @@
    + + + + @@ -107,6 +112,10 @@ + + + +
    FieldsComponents fields
    Events Component events Component init function
    get_text_width([text])Calculate text width with font with respect to trailing space
    set_to(set_to) Set text to text field
    set_pivot(pivot) Set text pivot.
    is_multiline()Return true, if text with line break

    Tables

    @@ -157,6 +166,30 @@ + +
    + + get_text_width([text]) +
    +
    + Calculate text width with font with respect to trailing space + + +

    Parameters:

    +
      +
    • text + string + + + + (optional) +
    • +
    + + + + +
    @@ -263,6 +296,26 @@ + +
    + + is_multiline() +
    +
    + Return true, if text with line break + + + +

    Returns:

    +
      + + boolean + Is text node with line break +
    + + + +

    Tables

    @@ -353,7 +406,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid.timer.html b/docs/modules/druid.timer.html index 97fd71e..3bf0f9d 100644 --- a/docs/modules/druid.timer.html +++ b/docs/modules/druid.timer.html @@ -68,6 +68,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -294,7 +295,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid_event.html b/docs/modules/druid_event.html index 81ef363..df96371 100644 --- a/docs/modules/druid_event.html +++ b/docs/modules/druid_event.html @@ -67,6 +67,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -240,7 +241,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/modules/druid_instance.html b/docs/modules/druid_instance.html index 5a9f0fb..0a4c8b4 100644 --- a/docs/modules/druid_instance.html +++ b/docs/modules/druid_instance.html @@ -67,6 +67,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • @@ -129,6 +130,22 @@ + + + + + + + + + + + + + + + + @@ -362,6 +379,67 @@ + +
    + + druid:on_focus_lost() +
    +
    + Druid on focus lost interest function. + This one called by onwindowcallback by global window listener + + + + + + + +
    +
    + + druid:on_focus_gained() +
    +
    + Druid on focus gained interest function. + This one called by onwindowcallback by global window listener + + + + + + + +
    +
    + + druid:on_layout_change() +
    +
    + Druid on layout change function. + Called on update gui layout + + + + + + + +
    +
    + + druid.on_language_change() +
    +
    + Druid on language change. + This one called by global gruid.onlanguagechange, but can be + call manualy to update all translations + + + + + + +
    @@ -802,7 +880,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/topics/01-components.md.html b/docs/topics/01-components.md.html index ac1bf54..9b1000c 100644 --- a/docs/topics/01-components.md.html +++ b/docs/topics/01-components.md.html @@ -58,6 +58,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • Modules

    @@ -279,8 +280,17 @@ Key is value from druid const: const.SIDE.X (or just "x") or const.SIDE.Y (or ju

    Basic Druid text input component (unimplemented)

    Setup

    +

    Create input component with druid: input = druid:new_input(button_node_name, text_node_name, keyboard_type)

    Notes

    +

    - Input component handle user text input. Input contains from button and text components. Button needed for selecting/unselecting input field +- Long click on input field for clear and select input field (clearing can be disable via styles) +- Click outside of button to unselect input field +- On focus lost (game minimized) input field will be unselected +- You can setup max length of the text +- You can setup allowed characters. On add not allowed characters on_input_wrong will be called. By default it cause simple shake animation +- The keyboard for input will not show on mobile HTML5. So input field in mobile HTML5 is not working now +- To make work different keyboard type, make sure value in game.project Android:InputMethod set to HidderInputField (https://defold.com/manuals/project-settings/#input-method)

    @@ -402,7 +412,7 @@ Key is value from druid const: const.SIDE.X (or just "x") or const.SIDE.Y (or ju
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/topics/02-creating_custom_components.md.html b/docs/topics/02-creating_custom_components.md.html index 478ff31..0251ceb 100644 --- a/docs/topics/02-creating_custom_components.md.html +++ b/docs/topics/02-creating_custom_components.md.html @@ -46,6 +46,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • Modules

    @@ -115,8 +116,8 @@ function M.on_message(self, message_id, message, sender) end --- Call only if component with ON_CHANGE_LANGUAGE interest -function M.on_change_language(self) +-- Call only if component with ON_LANGUAGE_CHANGE interest +function M.on_language_change(self) end -- Call only if component with ON_LAYOUT_CHANGE interest @@ -128,6 +129,14 @@ function M.on_input_interrupt(self) end +-- Call, if game lost focus. Need ON_FOCUS_LOST intereset +function M.on_focus_lost(self) +end + +-- Call, if game gained focus. Need ON_FOCUS_GAINED intereset +function M.on_focus_gained(self) +end + -- Call on component remove or on druid:final function M.on_remove(self) end @@ -178,11 +187,12 @@ There is next interests in druid:
  • ON_UPDATE - component will be updated from update

  • ONINPUTHIGH - component will receive input from oninput, before other components with ONINPUT

  • ON_INPUT - component will receive input from oninput, after other components with ONINPUT_HIGH

  • -
  • ONCHANGELANGUAGE - will call onchangelanguage function on language change trigger

  • -
  • ONLAYOUTCHANGED will call onlayoutchange function on layout change trigger

  • +
  • ONLANGUAGECHANGE - will call onlanguagechange function on language change trigger

  • +
  • ONLAYOUTCHANGE will call onlayoutchange function on layout change trigger

  • +
  • ONFOCUSLOST will call onfocustlost function in on focus lost event. You need to pass window_callback to global druid:on_window_callback

  • +
  • ONFOCUSGAINED will call onfocustgained function in on focus gained event. You need to pass window_callback to global druid:on_window_callback

  • -

    Best practice on custom components

    On each component recommended describe component scheme in next way:

    @@ -234,7 +244,7 @@ There is next interests in druid:
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/topics/03-styles.md.html b/docs/topics/03-styles.md.html index b3e69bd..8d2a391 100644 --- a/docs/topics/03-styles.md.html +++ b/docs/topics/03-styles.md.html @@ -34,7 +34,6 @@ @@ -45,6 +44,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • Modules

    @@ -89,7 +89,7 @@

    Usage

    -

    Setup default druid style for all druid instances via druid.set_default_style

    +

    Setup default druid style for all druid instances via druid.set_default_style

     local druid = require("druid.druid")
    @@ -129,17 +129,12 @@
     
    -

    -

    Create custom components

    -

    Styles is just lua table, so it can be described in just one single file -TODO

    -
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/topics/04-druid_assets.md.html b/docs/topics/04-druid_assets.md.html index f1c4cd7..227822d 100644 --- a/docs/topics/04-druid_assets.md.html +++ b/docs/topics/04-druid_assets.md.html @@ -43,6 +43,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • Modules

    @@ -90,7 +91,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/topics/05-examples.md.html b/docs/topics/05-examples.md.html index 12b20c5..28a3d17 100644 --- a/docs/topics/05-examples.md.html +++ b/docs/topics/05-examples.md.html @@ -43,6 +43,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • Modules

    @@ -88,7 +89,7 @@
    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/topics/README.md.html b/docs/topics/README.md.html index d993a24..f57e3ae 100644 --- a/docs/topics/README.md.html +++ b/docs/topics/README.md.html @@ -53,6 +53,7 @@
  • Styles
  • Druid assets
  • Examples
  • +
  • changelog
  • README
  • Modules

    @@ -110,11 +111,14 @@
    • Mouse trigger - Button 1 -> touch For basic input components
    • -
    • Key trigger - Backspace -> back For backhandler component_
    • -
    • Key trigger - Back -> back For backhandler component, Android back button_
    • +
    • Key trigger - Backspace -> key_backspace For backhandler component, input component_
    • +
    • Key trigger - Back -> key_back For backhandler component, Android back button, input component_
    • +
    • Key trigger - Enter -> key_enter For input component, optional
    • +
    • Key trigger - Esc -> key_esc For input component, optional
    -

    +

    +

    Input capturing [optional]

    @@ -144,6 +148,18 @@ -- Used for change default druid style druid.set_default_style(your_style) + +-- Call this function on language changing in the game, +-- to retranslate all lang_text components: +druid.on_languge_change() + +-- Call this function on layout changing in the game, +-- to reapply layouts +druid.on_layout_change() + +-- Call this function inside window.set_listener +-- to catch game focus lost/gained callbacks: +druid.on_window_callback(event) @@ -192,11 +208,15 @@ print("Button was clicked!") end -local function init(self) +function init(self) self.druid = druid.new(self) self.druid:new_button("button_node_name", button_callback) end +function final(self) + self.druid:final() +end + function on_input(self, action_id, action) return self.druid:on_input(action_id, action) end @@ -316,7 +336,7 @@ https://insality.github.io/druid/

    generated by LDoc 1.4.6 -Last updated 2020-04-17 20:13:54 +Last updated 2020-04-18 14:39:17
    diff --git a/docs/topics/changelog.md.html b/docs/topics/changelog.md.html new file mode 100644 index 0000000..9eba3e8 --- /dev/null +++ b/docs/topics/changelog.md.html @@ -0,0 +1,132 @@ + + + + + Defold Druid UI Library + + + + +
    + +
    + +
    +
    +
    + + +
    + + + + + + +
    + + +

    Druid 0.3.0:

    + +
      +
    • Druid:final now is important function for correct working

    • +
    • Add swipe basic component

      + +
      +- Swipe component handle simple swipe gestures on node. It has single callback with direction on swipe. You can adjust a several parameters of swipe in druid style.
      +- Swipe can be triggered on action.released or while user is make swiping (in process)
      +- Add swipe example at main Druid example. Try swipe left/right to switch example pages.
      +
      +
    • +
    • Add input basic component

      + +
      +- Input component handle user text input. Input contains from button and text components. Button needed for selecting/unselecting input field
      +- Long click on input field for clear and select input field (clearing can be disable via styles)
      +- Click outside of button to unselect input field
      +- On focus lost (game minimized) input field will be unselected
      +- You can setup max length of the text
      +- You can setup allowed characters. On add not allowed characters on_input_wrong will be called. By default it cause simple shake animation
      +- The keyboard for input will not show on mobile HTML5. So input field in mobile HTML5 is not working now
      +- To make work different keyboard type, make sure value in game.project Android:InputMethod set to HidderInputField (https://defold.com/manuals/project-settings/#input-method)
      +
      +
    • +
    • Add button onclickoutside event. You can subscribe on this event in button. Was needed for Input component (click outside to deselect input field).

    • +
    • Add start_pos to button component

    • +
    • Changed input binding settings. Add backspace, enter, text and marked_text. Backspace now is different from android back button.

    • +
    • Renamed onchangelanguage -> onlanguagechange component interest

    • +
    • Add basic component two functions: increase_input_priority and reset_input_priority. It used to process component input first in current input stack (there is two input stacks: INPUT and INPUT_HIGH). Example: on selecting input field, it increase input self priority until it be unselected

    • +
    • Add two new component interests: on_focus_gain and on_focus_lost

    • +
    • Add global druid events:

      + +
      +- on_window_callback: call druid.on_window_callback(event) for on_focus_gain/lost correct work
      +- on_language_change: call druid.on_language_change() (#38) for update all druid instances lang components
      +- on_layout_change: call druid.on_layout_change() (#37) for update all gui layouts (unimplemented now)
      +
      +
    • +
    • Add several examples to druid-assets respository

    • +
    • Known issues:

      + +
      +- Adjusting text size by height works wrong. Adjusting single line texting works fine
      +
      +
    • +
    + + +
    +
    +
    +generated by LDoc 1.4.6 +Last updated 2020-04-18 14:39:17 +
    +
    + +
    Druid on_message function
    druid:on_focus_lost()Druid on focus lost interest function.
    druid:on_focus_gained()Druid on focus gained interest function.
    druid:on_layout_change()Druid on layout change function.
    druid.on_language_change()Druid on language change.
    druid:new_button(...) Create button basic component
  • Styles