diff --git a/README.md b/README.md index ff9622a..6831790 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![](media/druid_logo.png) +[![](media/druid_logo.png)](https://AGulev.github.io/druid/) _travis/release bages_ **Druid** - powerful defold component UI library. Use standart components or make your own game-specific to make amazing GUI in your games. @@ -102,7 +102,7 @@ Basic custom component template looks like this: local const = require("druid.const") local component = require("druid.system.component") -local M = component.new("amazing_component", { const.ON_INPUT }) +local M = component.create("amazing_component", { const.ON_INPUT }) function M.init(self, ...) -- Component constructor @@ -139,7 +139,7 @@ On each component recomended describe component schema in next way: local helper = require("druid.helper") local component = require("druid.system.component") -local M = component.new("new_component") +local M = component.create("new_component") local SCHEME = { ROOT = "/root", @@ -172,8 +172,25 @@ end You can check our example here _TODO_ +## Reserver componeney keywords +- initialize +- init +- update +- on_input +- on_message +- on_swipe +- setup_component +- get_style +- set_style +- set_template +- set_nodes +- get_context +- set_context +- get_druid + ## API _Link to ldoc_ +[API](https://AGulev.github.io/druid/) ## Internal Generate with `ldoc .` with `config.ld` file. [Instructions](https://github.com/stevedonovan/LDoc) @@ -183,6 +200,7 @@ _TODO_ ## License +Using [middleclass by kikito](https://github.com/kikito/middleclass) MIT License diff --git a/druid/base/back_handler.lua b/druid/base/back_handler.lua index bd4078e..e957989 100644 --- a/druid/base/back_handler.lua +++ b/druid/base/back_handler.lua @@ -4,7 +4,7 @@ local const = require("druid.const") local component = require("druid.system.component") -local M = component.new("back_handler", { const.ON_INPUT }) +local M = component.create("back_handler", { const.ON_INPUT }) --- Component init function diff --git a/druid/base/blocker.lua b/druid/base/blocker.lua index 3011f1a..a599d7e 100644 --- a/druid/base/blocker.lua +++ b/druid/base/blocker.lua @@ -5,7 +5,7 @@ local const = require("druid.const") local helper = require("druid.helper") local component = require("druid.system.component") -local M = component.new("blocker", { const.ON_SWIPE }) +local M = component.create("blocker", { const.ON_SWIPE }) function M.init(self, node) diff --git a/druid/base/button.lua b/druid/base/button.lua index 67c485e..d340933 100644 --- a/druid/base/button.lua +++ b/druid/base/button.lua @@ -9,7 +9,8 @@ local const = require("druid.const") local helper = require("druid.helper") local component = require("druid.system.component") -local M = component.new("button", { const.ON_INPUT }) +local M = component.create("button", { const.ON_INPUT }) + --- Component init function -- @function button:init @@ -21,6 +22,7 @@ local M = component.new("button", { const.ON_INPUT }) -- @tparam[opt] string event Button react event, const.ACTION_TOUCH by default function M.init(self, node, callback, params, anim_node, event) assert(callback, "Button should have callback. To block input on zone use blocker component") + self.style = self:get_style() self.node = helper.get_node(node) diff --git a/druid/base/checkbox.lua b/druid/base/checkbox.lua index 1a52ad0..ef2f5d1 100644 --- a/druid/base/checkbox.lua +++ b/druid/base/checkbox.lua @@ -4,7 +4,7 @@ local helper = require("druid.helper") local component = require("druid.system.component") -local M = component.new("checkbox") +local M = component.create("checkbox") function M.set_state(self, state, is_silence) diff --git a/druid/base/checkbox_group.lua b/druid/base/checkbox_group.lua index 03faf8e..1360a3b 100644 --- a/druid/base/checkbox_group.lua +++ b/druid/base/checkbox_group.lua @@ -3,7 +3,7 @@ local component = require("druid.system.component") -local M = component.new("checkbox_group") +local M = component.create("checkbox_group") local function on_checkbox_click(self, index) diff --git a/druid/base/grid.lua b/druid/base/grid.lua index 74c4d66..34ac83a 100644 --- a/druid/base/grid.lua +++ b/druid/base/grid.lua @@ -5,7 +5,7 @@ local helper = require("druid.helper") local component = require("druid.system.component") -local M = component.new("grid") +local M = component.create("grid") function M.init(self, parent, element, in_row) diff --git a/druid/base/locale.lua b/druid/base/locale.lua index ec73bf9..effd5b3 100644 --- a/druid/base/locale.lua +++ b/druid/base/locale.lua @@ -6,7 +6,7 @@ local const = require("druid.const") local settings = require("druid.system.settings") local component = require("druid.system.component") -local M = component.new("locale", { const.ON_CHANGE_LANGUAGE }) +local M = component.create("locale", { const.ON_CHANGE_LANGUAGE }) function M.init(self, node, lang_id, no_adjust) diff --git a/druid/base/progress.lua b/druid/base/progress.lua index 564cebb..f7b1ac6 100644 --- a/druid/base/progress.lua +++ b/druid/base/progress.lua @@ -5,7 +5,7 @@ local const = require("druid.const") local helper = require("druid.helper") local component = require("druid.system.component") -local M = component.new("progress", { const.ON_UPDATE }) +local M = component.create("progress", { const.ON_UPDATE }) --- Component init function diff --git a/druid/base/radio_group.lua b/druid/base/radio_group.lua index 4802ca8..2b0606b 100644 --- a/druid/base/radio_group.lua +++ b/druid/base/radio_group.lua @@ -3,7 +3,7 @@ local component = require("druid.system.component") -local M = component.new("radio_group") +local M = component.create("radio_group") local function on_checkbox_click(self, index) diff --git a/druid/base/scroll.lua b/druid/base/scroll.lua index 7716e3e..1e3a943 100644 --- a/druid/base/scroll.lua +++ b/druid/base/scroll.lua @@ -5,7 +5,7 @@ local helper = require("druid.helper") local const = require("druid.const") local component = require("druid.system.component") -local M = component.new("scroll", { const.ON_UPDATE, const.ON_SWIPE }) +local M = component.create("scroll", { const.ON_UPDATE, const.ON_SWIPE }) -- Global on all scrolls diff --git a/druid/base/slider.lua b/druid/base/slider.lua index 8503501..fd63782 100644 --- a/druid/base/slider.lua +++ b/druid/base/slider.lua @@ -5,7 +5,7 @@ local helper = require("druid.helper") local const = require("druid.const") local component = require("druid.system.component") -local M = component.new("slider", { const.ON_SWIPE }) +local M = component.create("slider", { const.ON_SWIPE }) local function on_change_value(self) diff --git a/druid/base/text.lua b/druid/base/text.lua index f7db024..d0dff8a 100644 --- a/druid/base/text.lua +++ b/druid/base/text.lua @@ -6,7 +6,7 @@ local const = require("druid.const") local helper = require("druid.helper") local component = require("druid.system.component") -local M = component.new("text") +local M = component.create("text") function M.init(self, node, value, no_adjust) diff --git a/druid/base/timer.lua b/druid/base/timer.lua index 5c26410..8554818 100644 --- a/druid/base/timer.lua +++ b/druid/base/timer.lua @@ -6,7 +6,7 @@ local formats = require("druid.helper.formats") local helper = require("druid.helper") local component = require("druid.system.component") -local M = component.new("timer", { const.ON_UPDATE }) +local M = component.create("timer", { const.ON_UPDATE }) function M.init(self, node, seconds_from, seconds_to, callback) diff --git a/druid/druid.lua b/druid/druid.lua index c1f582d..3b4b8fe 100644 --- a/druid/druid.lua +++ b/druid/druid.lua @@ -29,7 +29,14 @@ M.components = { checkbox_group = require("druid.base.checkbox_group"), radio_group = require("druid.base.radio_group"), + -- TODO: + -- input - text input + -- infinity_scroll -- to handle big data scroll + progress_rich = require("druid.rich.progress_rich"), + + -- TODO: Examples: + -- Slider menu like clash royale } @@ -52,8 +59,9 @@ function M.register(name, module) -- Possibly: druid.new(druid.BUTTON, etc?) -- Current way is very implicit druid_instance["new_" .. name] = function(self, ...) - return druid_instance.new(self, module, ...) + return druid_instance.create(self, module, ...) end + log("Register component", name) end @@ -65,20 +73,8 @@ function M.new(context, style) register_basic_components() register_basic_components = false end - local druid = setmetatable({}, { __index = druid_instance }) - -- Druid context here (who created druid) - -- Usually gui_script, but can be component from self:get_druid() - druid._context = context - druid._style = style or settings.default_style - - -- TODO: Find the better way to handle components - -- All component list - druid.all_components = {} - -- Map: interest: {components} - druid.components = {} - - return druid + return druid_instance(context, style) end @@ -90,6 +86,7 @@ end function M.set_text_function(callback) settings.get_text = callback or const.EMPTY_FUNCTION -- TODO: Update all localized text + -- TOOD: Need to store all current druid instances? end diff --git a/druid/rich/progress_rich.lua b/druid/rich/progress_rich.lua index 4a6d3e1..2c5de7c 100644 --- a/druid/rich/progress_rich.lua +++ b/druid/rich/progress_rich.lua @@ -3,7 +3,7 @@ local component = require("druid.system.component") -local M = component.new("progress_rich") +local M = component.create("progress_rich") function M.init(self, name, red, green, key) diff --git a/druid/system/component.lua b/druid/system/component.lua index b2d8d83..ccfe840 100644 --- a/druid/system/component.lua +++ b/druid/system/component.lua @@ -2,52 +2,12 @@ --@class component local const = require("druid.const") +local class = require("druid.system.middleclass") -local M = {} -local instance = {} +local Component = class("druid.component") -function instance.get_style(self) - if not self._meta.style then - return const.EMPTY_TABLE - end - - return self._meta.style[self._component.name] or const.EMPTY_TABLE -end - - -function instance.set_style(self, druid_style) - self._meta.style = druid_style -end - - -function instance.set_template(self, template) - self._meta.template = template -end - - -function instance.set_nodes(self, nodes) - self._meta.nodes = nodes -end - - -function instance.get_context(self, context) - return self._meta.context -end - - -function instance.set_context(self, context) - self._meta.context = context -end - - -function instance.get_druid(self) - local context = { _context = self } - return setmetatable(context, { __index = self:get_context().druid }) -end - - -function instance.setup_component(self, context, style) +function Component.setup_component(self, context, style) self._meta = { template = nil, context = nil, @@ -62,17 +22,99 @@ function instance.setup_component(self, context, style) end -function M.new(name, interest) - local mt = { - _component = { - name = name, - interest = interest - } - } - local component = setmetatable(mt, { __index = instance }) +function Component.get_style(self) + if not self._meta.style then + return const.EMPTY_TABLE + end - return component + return self._meta.style[self._component.name] or const.EMPTY_TABLE end -return M \ No newline at end of file +function Component.set_style(self, druid_style) + self._meta.style = druid_style +end + + +function Component.get_template(self) + return self._meta.template +end + + +function Component.set_template(self, template) + self._meta.template = template +end + + +function Component.get_nodes(self) + return self._meta.nodes +end + + +function Component.set_nodes(self, nodes) + self._meta.nodes = nodes +end + + +function Component.get_context(self, context) + return self._meta.context +end + + +function Component.set_context(self, context) + self._meta.context = context +end + + +function Component.get_interests(self) + return self._component.interest +end + + +-- TODO: Определиться с get_node и node +-- get_node - берет ноду по ноде или строке +-- node - может брать ноду у компонента по схеме (если есть +-- template или таблица нод после gui.clone_tree) +function Component.get_node(self, node_or_name) + local template_name = self:get_template() or const.EMPTY_STRING + local nodes = self:get_nodes() + + if nodes then + return nodes[template_name .. node_or_name] + else + if type(node_or_name) == const.STRING then + return gui.get_node(template_name .. node_or_name) + else + return node_or_name + end + end +end + + +function Component.get_druid(self) + local context = { _context = self } + return setmetatable(context, { __index = self:get_context().druid }) +end + + +function Component.initialize(self, name, interest) + self._component = { + name = name, + interest = interest + } +end + + +function Component.static.create(name, interest) + -- Yea, inheritance here + local new_class = class(name, Component) + + new_class.initialize = function(self) + Component.initialize(self, name, interest) + end + + return new_class +end + + +return Component diff --git a/druid/system/druid_instance.lua b/druid/system/druid_instance.lua index 35abac6..80a27a2 100644 --- a/druid/system/druid_instance.lua +++ b/druid/system/druid_instance.lua @@ -1,8 +1,9 @@ local const = require("druid.const") local druid_input = require("druid.helper.druid_input") local settings = require("druid.system.settings") +local class = require("druid.system.middleclass") -local M = {} +local Druid = class("druid.druid_instance") local function input_init(self) @@ -19,16 +20,15 @@ end -- Create the component -local function create(self, module) +local function create(self, instance_class) ---@class component - local instance = setmetatable({}, { __index = module }) + local instance = instance_class() -- Component context, self from component creation instance:setup_component(self._context, self._style) - table.insert(self.all_components, instance) - local register_to = module._component.interest + local register_to = instance:get_interests() if register_to then for i = 1, #register_to do local interest = register_to[i] @@ -47,51 +47,6 @@ local function create(self, module) end -function M.new(self, module, ...) - local instance = create(self, module) - - if instance.init then - instance:init(...) - end - - return instance -end - - -function M.remove(self, instance) - for i = #self.all_components, 1, -1 do - if self.all_components[i] == instance then - table.remove(self, i) - end - end - - local interests = instance._component.interest - if interests then - for i = 1, #interests do - local interest = interests[i] - local array = self.components[interest] - for j = #array, 1, -1 do - if array[j] == instance then - table.remove(array, j) - end - end - end - end -end - - ---- Druid instance update function --- @function druid:update(dt) -function M.update(self, dt) - local array = self.components[const.ON_UPDATE] - if array then - for i = 1, #array do - array[i]:update(dt) - end - end -end - - local function notify_input_on_swipe(self) if self.components[const.ON_INPUT] then local len = #self.components[const.ON_INPUT] @@ -118,17 +73,76 @@ local function match_event(action_id, events) end +--- Druid constructor +function Druid.initialize(self, context, style) + self._context = context + self._style = style or settings.default_style + self.all_components = {} + self.components = {} +end + + +--- Create new component inside druid instance +function Druid.create(self, instance_class, ...) + local instance = create(self, instance_class) + + if instance.init then + instance:init(...) + end + + return instance +end + + +--- Remove component from druid instance +-- It will call on_remove on component, if exist +function Druid.remove(self, component) + for i = #self.all_components, 1, -1 do + if self.all_components[i] == component then + if component.on_remove then + component:on_remove() + end + table.remove(self, i) + end + end + + local interests = component:get_interests() + if interests then + for i = 1, #interests do + local interest = interests[i] + local array = self.components[interest] + for j = #array, 1, -1 do + if array[j] == component then + table.remove(array, j) + end + end + end + end +end + + +--- Druid instance update function +-- @function druid:update(dt) +function Druid.update(self, dt) + local array = self.components[const.ON_UPDATE] + if array then + for i = 1, #array do + array[i]:update(dt) + end + end +end + + --- Druid instance on_input function -- @function druid:on_input(action_id, action) -function M.on_input(self, action_id, action) +function Druid.on_input(self, action_id, action) -- TODO: расписать отличия ON_SWIPE и ON_INPUT -- Почему-то некоторые используют ON_SWIPE, а логичнее ON_INPUT? (blocker, slider) local array = self.components[const.ON_SWIPE] if array then - local v, result - local len = #array - for i = len, 1, -1 do - v = array[i] + local result + for i = #array, 1, -1 do + local v = array[i] result = result or v:on_input(action_id, action) end if result then @@ -139,42 +153,38 @@ function M.on_input(self, action_id, action) array = self.components[const.ON_INPUT] if array then - local v - local len = #array - for i = len, 1, -1 do - v = array[i] + for i = #array, 1, -1 do + local v = array[i] if match_event(action_id, v.event) and v:on_input(action_id, action) then return true end end return false end + return false end --- Druid instance on_message function -- @function druid:on_message(message_id, message, sender) -function M.on_message(self, message_id, message, sender) +function Druid.on_message(self, message_id, message, sender) local specific_ui_message = const.SPECIFIC_UI_MESSAGES[message_id] if specific_ui_message then local array = self.components[message_id] if array then - local item for i = 1, #array do - item = array[i] + local item = array[i] item[specific_ui_message](item, message, sender) end end else - local array = self.components[const.ON_MESSAGE] - if array then - for i = 1, #array do - array[i]:on_message(message_id, message, sender) - end + local array = self.components[const.ON_MESSAGE] or const.EMPTY_TABLE + for i = 1, #array do + array[i]:on_message(message_id, message, sender) end end end -return M +return Druid diff --git a/druid/system/middleclass.lua b/druid/system/middleclass.lua new file mode 100644 index 0000000..7e36bcd --- /dev/null +++ b/druid/system/middleclass.lua @@ -0,0 +1,183 @@ +local middleclass = { + _VERSION = 'middleclass v4.1.1', + _DESCRIPTION = 'Object Orientation for Lua', + _URL = 'https://github.com/kikito/middleclass', + _LICENSE = [[ + MIT LICENSE + + Copyright (c) 2011 Enrique García Cota + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ]] +} + +local function _createIndexWrapper(aClass, f) + if f == nil then + return aClass.__instanceDict + else + return function(self, name) + local value = aClass.__instanceDict[name] + + if value ~= nil then + return value + elseif type(f) == "function" then + return (f(self, name)) + else + return f[name] + end + end + end +end + +local function _propagateInstanceMethod(aClass, name, f) + f = name == "__index" and _createIndexWrapper(aClass, f) or f + aClass.__instanceDict[name] = f + + for subclass in pairs(aClass.subclasses) do + if rawget(subclass.__declaredMethods, name) == nil then + _propagateInstanceMethod(subclass, name, f) + end + end +end + +local function _declareInstanceMethod(aClass, name, f) + aClass.__declaredMethods[name] = f + + if f == nil and aClass.super then + f = aClass.super.__instanceDict[name] + end + + _propagateInstanceMethod(aClass, name, f) +end + +local function _tostring(self) return "class " .. self.name end +local function _call(self, ...) return self:new(...) end + +local function _createClass(name, super) + local dict = {} + dict.__index = dict + + local aClass = { name = name, super = super, static = {}, + __instanceDict = dict, __declaredMethods = {}, + subclasses = setmetatable({}, {__mode='k'}) } + + if super then + setmetatable(aClass.static, { + __index = function(_,k) + local result = rawget(dict,k) + if result == nil then + return super.static[k] + end + return result + end + }) + else + setmetatable(aClass.static, { __index = function(_,k) return rawget(dict,k) end }) + end + + setmetatable(aClass, { __index = aClass.static, __tostring = _tostring, + __call = _call, __newindex = _declareInstanceMethod }) + + return aClass +end + +local function _includeMixin(aClass, mixin) + assert(type(mixin) == 'table', "mixin must be a table") + + for name,method in pairs(mixin) do + if name ~= "included" and name ~= "static" then aClass[name] = method end + end + + for name,method in pairs(mixin.static or {}) do + aClass.static[name] = method + end + + if type(mixin.included)=="function" then mixin:included(aClass) end + return aClass +end + +local DefaultMixin = { + __tostring = function(self) return "instance of " .. tostring(self.class) end, + + initialize = function(self, ...) end, + + isInstanceOf = function(self, aClass) + return type(aClass) == 'table' + and type(self) == 'table' + and (self.class == aClass + or type(self.class) == 'table' + and type(self.class.isSubclassOf) == 'function' + and self.class:isSubclassOf(aClass)) + end, + + static = { + allocate = function(self) + assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'") + return setmetatable({ class = self }, self.__instanceDict) + end, + + new = function(self, ...) + assert(type(self) == 'table', "Make sure that you are using 'Class:new' instead of 'Class.new'") + local instance = self:allocate() + instance:initialize(...) + return instance + end, + + subclass = function(self, name) + assert(type(self) == 'table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'") + assert(type(name) == "string", "You must provide a name(string) for your class") + + local subclass = _createClass(name, self) + + for methodName, f in pairs(self.__instanceDict) do + _propagateInstanceMethod(subclass, methodName, f) + end + subclass.initialize = function(instance, ...) return self.initialize(instance, ...) end + + self.subclasses[subclass] = true + self:subclassed(subclass) + + return subclass + end, + + subclassed = function(self, other) end, + + isSubclassOf = function(self, other) + return type(other) == 'table' and + type(self.super) == 'table' and + ( self.super == other or self.super:isSubclassOf(other) ) + end, + + include = function(self, ...) + assert(type(self) == 'table', "Make sure you that you are using 'Class:include' instead of 'Class.include'") + for _,mixin in ipairs({...}) do _includeMixin(self, mixin) end + return self + end + } +} + +function middleclass.class(name, super) + assert(type(name) == 'string', "A name (string) is needed for the new class") + return super and super:subclass(name) or _includeMixin(_createClass(name), DefaultMixin) +end + +setmetatable(middleclass, { __call = function(_, ...) return middleclass.class(...) end }) + +return middleclass diff --git a/druid/system/settings.lua b/druid/system/settings.lua index 637339d..1ca7dbe 100644 --- a/druid/system/settings.lua +++ b/druid/system/settings.lua @@ -9,13 +9,11 @@ M.auto_focus_gain = true function M.get_text(name) - -- override to get text for localized text return "[Druid]: locales not inited" end function M.play_sound(name) - -- override to play sound with name end