From 94d1d64dc3c06135b7840ee1d6442463170486ac Mon Sep 17 00:00:00 2001 From: Insality Date: Wed, 5 Mar 2025 21:19:03 +0200 Subject: [PATCH] Update button docs --- docs_md/api_reference/druid_button.md | 1 - docs_md/components/button_api.md | 16 + docs_md/components/button_manual.md | 0 druid/base/button.lua | 483 +++++++----------- druid/component.lua | 25 +- druid/system/druid_instance.lua | 13 +- example/examples/data_list/examples_list.lua | 2 +- .../widgets/fps_panel/example_fps_panel.lua | 2 +- 8 files changed, 217 insertions(+), 325 deletions(-) delete mode 100644 docs_md/api_reference/druid_button.md create mode 100644 docs_md/components/button_api.md create mode 100644 docs_md/components/button_manual.md diff --git a/docs_md/api_reference/druid_button.md b/docs_md/api_reference/druid_button.md deleted file mode 100644 index e072063..0000000 --- a/docs_md/api_reference/druid_button.md +++ /dev/null @@ -1 +0,0 @@ -# Druid Button API Reference diff --git a/docs_md/components/button_api.md b/docs_md/components/button_api.md new file mode 100644 index 0000000..815af85 --- /dev/null +++ b/docs_md/components/button_api.md @@ -0,0 +1,16 @@ +# Button Quick API reference + +```lua +button:get_key_trigger() +button:init(node_or_node_id, [callback], [custom_args], [anim_node]) +button:is_enabled() +button:on_input([action_id], [action]) +button:on_input_interrupt() +button:on_late_init() +button:on_style_change(style) +button:set_check_function([check_function], [failure_callback]) +button:set_click_zone([zone]) +button:set_enabled([state]) +button:set_key_trigger(key) +button:set_web_user_interaction([is_web_mode]) +``` diff --git a/docs_md/components/button_manual.md b/docs_md/components/button_manual.md new file mode 100644 index 0000000..e69de29 diff --git a/druid/base/button.lua b/druid/base/button.lua index f454001..5674df6 100755 --- a/druid/base/button.lua +++ b/druid/base/button.lua @@ -1,286 +1,9 @@ --- Copyright (c) 2021 Maksim Tuprikov . This code is licensed under MIT license - ---- Druid Component for Handling User Click Interactions: Click, Long Click, Double Click, and More. --- --- # Overview # --- --- This component provides a versatile solution for handling user click interactions. --- It allows you to make any GUI node clickable and define various callbacks for different types of clicks. --- --- # Notes # --- --- • The click callback will not trigger if the cursor moves outside the node's --- area between the pressed and released states. --- --- • If a button has a double click event subscriber and the double click event is triggered, --- the regular click callback will not be triggered. --- --- • Buttons can be triggered using a keyboard key by calling the button:set_key_trigger method. --- --- • To animate a small icon on a big button panel, you can use an animation node. --- The trigger node name should be set as "big panel," and the animation node should be set as "small icon." --- --- Example Link --- @usage --- local function on_button_click(self, args, button) --- print("Button has clicked with params: " .. args) --- print("Also the button component is passed in callback params") --- end --- --- local custom_args = "Any variable to pass inside callback" --- local button = self.druid:new_button("button_name", on_button_click, custom_args) --- --- @module Button --- @within BaseComponent --- @alias druid.button - - ---- The event: Event on successful release action over button. --- @usage --- -- Custom args passed in Button constructor --- button.on_click:subscribe(function(self, custom_args, button_instance) --- print("On button click!") --- end) --- @tfield event on_click event - - ---- The event: Event on repeated action over button. --- --- This callback will be triggered if user hold the button. The repeat rate pick from `input.repeat_interval` in game.project --- @usage --- -- Custom args passed in Button constructor --- button.on_repeated_click:subscribe(function(self, custom_args, button_instance, click_count) --- print("On repeated Button click!") --- end) --- @tfield event on_repeated_click event - - ---- The event: Event on long tap action over button. --- --- This callback will be triggered if user pressed the button and hold the some amount of time. --- The amount of time picked from button style param: LONGTAP_TIME --- @usage --- -- Custom args passed in Button constructor --- button.on_long_click:subscribe(function(self, custom_args, button_instance, hold_time) --- print("On long Button click!") --- end) --- @tfield event on_long_click event - - ---- The event: Event on double tap action over button. --- --- If secondary click was too fast after previous one, the double --- click will be called instead usual click (if on_double_click subscriber exists) --- @usage --- -- Custom args passed in Button constructor --- button.on_double_click:subscribe(function(self, custom_args, button_instance, click_amount) --- print("On double Button click!") --- end) --- @tfield event on_double_click event - - ---- The event: Event calls every frame before on_long_click event. --- --- If long_click subscriber exists, the on_hold_callback will be called before long_click trigger. --- --- Usecase: Animate button progress of long tap --- @usage --- -- Custom args passed in Button constructor --- button.on_double_click:subscribe(function(self, custom_args, button_instance, time) --- print("On hold Button callback!") --- end) --- @tfield event on_hold_callback event - - ---- The event: Event calls if click event was outside of button. --- --- This event will be triggered for each button what was not clicked on user click action --- --- Usecase: Hide the popup when click outside --- @usage --- -- Custom args passed in Button constructor --- button.on_click_outside:subscribe(function(self, custom_args, button_instance) --- print("On click Button outside!") --- end) --- @tfield event on_click_outside event - - ---- The event: Event triggered if button was pressed by user. --- @usage --- -- Custom args passed in Button constructor --- button.on_pressed:subscribe(function(self, custom_args, button_instance) --- print("On Button pressed!") --- end) --- @tfield event on_pressed event - ---- Button trigger node --- @tfield node node - ----The GUI node id from button node --- @tfield hash node_id - ---- Button animation node. --- In default case equals to clickable node. --- --- Usecase: You have the big clickable panel, but want to animate only one small icon on it. --- @tfield node|nil anim_node Default node - ----Custom args for any Button event. Setup in Button constructor --- @tfield any params - ---- The Hover: Button Hover component --- @tfield Hover hover Hover - ---- Additional button click area, defined by another GUI node --- @tfield node|nil click_zone - ---- - local event = require("event.event") local const = require("druid.const") local helper = require("druid.helper") local component = require("druid.component") ----Clickable node with various interaction callbacks ----@class druid.button: druid.component ----@field on_click event function(self, custom_args, button_instance) ----@field on_pressed event ----@field on_repeated_click event ----@field on_long_click event ----@field on_double_click event ----@field on_hold_callback event ----@field on_click_outside event ----@field node node ----@field node_id hash ----@field anim_node node ----@field params any ----@field hover druid.hover ----@field click_zone node|nil ----@field start_scale vector3 ----@field start_pos vector3 ----@field disabled boolean ----@field key_trigger hash ----@field style table -local M = component.create("button") - - -local function is_input_match(self, action_id) - if action_id == const.ACTION_TOUCH or action_id == const.ACTION_MULTITOUCH then - return true - end - - if self.key_trigger and action_id == self.key_trigger then - return true - end - - return false -end - - -local function on_button_hover(self, hover_state) - self.style.on_hover(self, self.anim_node, hover_state) -end - - -local function on_button_mouse_hover(self, hover_state) - self.style.on_mouse_hover(self, self.anim_node, hover_state) -end - - -local function on_button_click(self) - if self._is_html5_mode then - self._is_html5_listener_set = false - html5.set_interaction_listener(nil) - end - self.click_in_row = 1 - self.on_click:trigger(self:get_context(), self.params, self) - self.style.on_click(self, self.anim_node) -end - - -local function on_button_repeated_click(self) - if not self.is_repeated_started then - self.click_in_row = 0 - self.is_repeated_started = true - end - - self.click_in_row = self.click_in_row + 1 - self.on_repeated_click:trigger(self:get_context(), self.params, self, self.click_in_row) - self.style.on_click(self, self.anim_node) -end - - -local function on_button_long_click(self) - self.click_in_row = 1 - local time = socket.gettime() - self.last_pressed_time - self.on_long_click:trigger(self:get_context(), self.params, self, time) - self.style.on_click(self, self.anim_node) -end - - -local function on_button_double_click(self) - self.click_in_row = self.click_in_row + 1 - self.on_double_click:trigger(self:get_context(), self.params, self, self.click_in_row) - self.style.on_click(self, self.anim_node) -end - - -local function on_button_hold(self, press_time) - self.on_hold_callback:trigger(self:get_context(), self.params, self, press_time) -end - - ----@param self druid.button -local function on_button_release(self) - if self.is_repeated_started then - return false - end - - local check_function_result = true - if self._check_function then - check_function_result = self._check_function(self:get_context()) - end - - if self.disabled then - self.style.on_click_disabled(self, self.anim_node) - return true - elseif not check_function_result then - if self._failure_callback then - self._failure_callback(self:get_context()) - end - return true - else - if self.can_action and not self._is_html5_mode then - self.can_action = false - - local time = socket.gettime() - local is_long_click = (time - self.last_pressed_time) >= self.style.LONGTAP_TIME - is_long_click = is_long_click and not self.on_long_click:is_empty() - - local is_double_click = (time - self.last_released_time) < self.style.DOUBLETAP_TIME - is_double_click = is_double_click and not self.on_double_click:is_empty() - - if is_long_click then - local is_hold_complete = (time - self.last_pressed_time) >= self.style.AUTOHOLD_TRIGGER - if is_hold_complete then - on_button_long_click(self) - else - self.on_click_outside:trigger(self:get_context(), self.params, self) - end - elseif is_double_click then - on_button_double_click(self) - else - on_button_click(self) - end - - self.last_released_time = time - end - return true - end -end - - ---- Component style params. +---Button style params. ---You can override this component styles params in Druid styles table ---or create your own style ---@class druid.button.style @@ -293,18 +16,43 @@ end ---@field on_mouse_hover fun(self, node, hover_state)|nil ---@field on_set_enabled fun(self, node, enabled_state)|nil + +---Clickable node with various interaction callbacks +---@class druid.button: druid.component +---@field on_click event function(self, custom_args, button_instance) +---@field on_pressed event function(self, custom_args, button_instance) +---@field on_repeated_click event function(self, custom_args, button_instance, click_count) +---@field on_long_click event function(self, custom_args, button_instance, hold_time) +---@field on_double_click event function(self, custom_args, button_instance, click_amount) +---@field on_hold_callback event function(self, custom_args, button_instance, press_time) +---@field on_click_outside event function(self, custom_args, button_instance) +---@field node node Clickable node +---@field node_id hash Node id +---@field anim_node node Animation node. In default case equals to clickable node +---@field params any Custom arguments for any Button event +---@field hover druid.hover Hover component for this button +---@field click_zone node|nil Click zone node to restrict click area +---@field start_scale vector3 Start scale of the button +---@field start_pos vector3 Start position of the button +---@field disabled boolean Is button disabled +---@field key_trigger hash Key trigger for this button +---@field style table Style for this button +local M = component.create("button") + + ---@param style druid.button.style function M:on_style_change(style) - self.style = {} - self.style.LONGTAP_TIME = style.LONGTAP_TIME or 0.4 - self.style.AUTOHOLD_TRIGGER = style.AUTOHOLD_TRIGGER or 0.8 - self.style.DOUBLETAP_TIME = style.DOUBLETAP_TIME or 0.4 + self.style = { + LONGTAP_TIME = style.LONGTAP_TIME or 0.4, + AUTOHOLD_TRIGGER = style.AUTOHOLD_TRIGGER or 0.8, + DOUBLETAP_TIME = style.DOUBLETAP_TIME or 0.4, - self.style.on_click = style.on_click or function(_, node) end - self.style.on_click_disabled = style.on_click_disabled or function(_, node) end - self.style.on_mouse_hover = style.on_mouse_hover or function(_, node, state) end - self.style.on_hover = style.on_hover or function(_, node, state) end - self.style.on_set_enabled = style.on_set_enabled or function(_, node, state) end + on_click = style.on_click or function(_, node) end, + on_click_disabled = style.on_click_disabled or function(_, node) end, + on_mouse_hover = style.on_mouse_hover or function(_, node, state) end, + on_hover = style.on_hover or function(_, node, state) end, + on_set_enabled = style.on_set_enabled or function(_, node, state) end, + } end @@ -322,8 +70,8 @@ function M:init(node_or_node_id, callback, custom_args, anim_node) self.start_scale = gui.get_scale(self.anim_node) self.start_pos = gui.get_position(self.anim_node) self.params = custom_args - self.hover = self.druid:new_hover(node_or_node_id, on_button_hover) - self.hover.on_mouse_hover:subscribe(on_button_mouse_hover) + self.hover = self.druid:new_hover(node_or_node_id, self._on_button_hover) + self.hover.on_mouse_hover:subscribe(self._on_button_mouse_hover) self.click_zone = nil self.is_repeated_started = false self.last_pressed_time = 0 @@ -357,8 +105,11 @@ function M:on_late_init() end +---@param action_id hash +---@param action table +---@return boolean function M:on_input(action_id, action) - if not is_input_match(self, action_id) then + if not self:_is_input_match(action_id) then return false end @@ -402,7 +153,7 @@ function M:on_input(action_id, action) if self._is_html5_mode then self._is_html5_listener_set = true html5.set_interaction_listener(function() - on_button_click(self) + self:_on_button_click() end) end return is_consume @@ -411,25 +162,25 @@ function M:on_input(action_id, action) -- While hold button, repeat rate pick from input.repeat_interval if action.repeated then if not self.on_repeated_click:is_empty() and self.can_action then - on_button_repeated_click(self) + self:_on_button_repeated_click() return is_consume end end if action.released then - return on_button_release(self) and is_consume + return self:_on_button_release() and is_consume end if self.can_action and not self.on_long_click:is_empty() then local press_time = socket.gettime() - self.last_pressed_time if self.style.AUTOHOLD_TRIGGER <= press_time then - on_button_release(self) + self:_on_button_release() return is_consume end if press_time >= self.style.LONGTAP_TIME then - on_button_hold(self, press_time) + self:_on_button_hold(press_time) return is_consume end end @@ -445,9 +196,9 @@ function M:on_input_interrupt() end ---- Set button enabled state. --- The style.on_set_enabled will be triggered. --- Disabled button is not clickable. +---Set button enabled state. +---The style.on_set_enabled will be triggered. +---Disabled button is not clickable. ---@param state boolean|nil Enabled state ---@return druid.button self function M:set_enabled(state) @@ -459,19 +210,17 @@ function M:set_enabled(state) end ---- Get button enabled state. --- --- By default all Buttons is enabled on creating. +---Get button enabled state. +---By default all Buttons is enabled on creating. ---@return boolean @True, if button is enabled now, False overwise function M:is_enabled() return not self.disabled end ---- Set additional button click area. --- Useful to restrict click outside out stencil node or scrollable content. --- --- This functions calls automatically if you don't disable it in game.project: druid.no_stencil_check +---Set additional button click area. +---Useful to restrict click outside out stencil node or scrollable content. +---This functions calls automatically if you don't disable it in game.project: druid.no_stencil_check ---@param zone node|string|nil Gui node ---@return druid.button self function M:set_click_zone(zone) @@ -496,7 +245,7 @@ function M:set_key_trigger(key) end ---- Get current key name to trigger this button. +---Get current key name to trigger this button. ---@return hash key_trigger The action_id of the input key function M:get_key_trigger() return self.key_trigger @@ -529,4 +278,126 @@ function M:set_web_user_interaction(is_web_mode) end + +---@param action_id hash +---@return boolean +function M:_is_input_match(action_id) + if action_id == const.ACTION_TOUCH or action_id == const.ACTION_MULTITOUCH then + return true + end + + if self.key_trigger and action_id == self.key_trigger then + return true + end + + return false +end + + +---@param hover_state boolean +function M:_on_button_hover(hover_state) + self.style.on_hover(self, self.anim_node, hover_state) +end + + +---@param hover_state boolean +function M:_on_button_mouse_hover(hover_state) + self.style.on_mouse_hover(self, self.anim_node, hover_state) +end + + +function M:_on_button_click() + if self._is_html5_mode then + self._is_html5_listener_set = false + html5.set_interaction_listener(nil) + end + self.click_in_row = 1 + self.on_click:trigger(self:get_context(), self.params, self) + self.style.on_click(self, self.anim_node) +end + + +function M:_on_button_repeated_click() + if not self.is_repeated_started then + self.click_in_row = 0 + self.is_repeated_started = true + end + + self.click_in_row = self.click_in_row + 1 + self.on_repeated_click:trigger(self:get_context(), self.params, self, self.click_in_row) + self.style.on_click(self, self.anim_node) +end + + +function M:_on_button_long_click() + self.click_in_row = 1 + local time = socket.gettime() - self.last_pressed_time + self.on_long_click:trigger(self:get_context(), self.params, self, time) + self.style.on_click(self, self.anim_node) +end + + +function M:_on_button_double_click() + self.click_in_row = self.click_in_row + 1 + self.on_double_click:trigger(self:get_context(), self.params, self, self.click_in_row) + self.style.on_click(self, self.anim_node) +end + + +---@param press_time number Amount of time the button was held +function M:_on_button_hold(press_time) + self.on_hold_callback:trigger(self:get_context(), self.params, self, press_time) +end + + +function M:_on_button_release() + if self.is_repeated_started then + return false + end + + local check_function_result = true + if self._check_function then + check_function_result = self._check_function(self:get_context()) + end + + if self.disabled then + self.style.on_click_disabled(self, self.anim_node) + return true + elseif not check_function_result then + if self._failure_callback then + self._failure_callback(self:get_context()) + end + return true + else + if self.can_action and not self._is_html5_mode then + self.can_action = false + + local time = socket.gettime() + local is_long_click = (time - self.last_pressed_time) >= self.style.LONGTAP_TIME + is_long_click = is_long_click and not self.on_long_click:is_empty() + + local is_double_click = (time - self.last_released_time) < self.style.DOUBLETAP_TIME + is_double_click = is_double_click and not self.on_double_click:is_empty() + + if is_long_click then + local is_hold_complete = (time - self.last_pressed_time) >= self.style.AUTOHOLD_TRIGGER + if is_hold_complete then + self:_on_button_long_click() + else + self.on_click_outside:trigger(self:get_context(), self.params, self) + end + elseif is_double_click then + self:_on_button_double_click() + else + self:_on_button_click() + end + + self.last_released_time = time + end + return true + end +end + + + return M diff --git a/druid/component.lua b/druid/component.lua index 6e709e7..6ae8656 100644 --- a/druid/component.lua +++ b/druid/component.lua @@ -21,18 +21,19 @@ local helper = require("druid.helper") ---@class druid.component ---@field druid druid.instance Druid instance to create inner components ----@field init fun(self:druid.component, ...)|nil ----@field update fun(self:druid.component, dt:number)|nil ----@field on_remove fun(self:druid.component)|nil ----@field on_input fun(self:druid.component, action_id:number, action:table)|nil ----@field on_message fun(self:druid.component, message_id:hash, message:table, sender:url)|nil ----@field on_late_init fun(self:druid.component)|nil ----@field on_focus_lost fun(self:druid.component)|nil ----@field on_focus_gained fun(self:druid.component)|nil ----@field on_style_change fun(self:druid.component, style: table)|nil ----@field on_layout_change fun(self:druid.component)|nil ----@field on_window_resized fun(self:druid.component)|nil ----@field on_language_change fun(self:druid.component)|nil +---@field init fun(self:druid.component, ...)|nil Called when component is created +---@field update fun(self:druid.component, dt:number)|nil Called every frame +---@field on_remove fun(self:druid.component)|nil Called when component is removed +---@field on_input fun(self:druid.component, action_id:hash, action:table)|nil Called when input event is triggered +---@field on_input_interrupt fun(self:druid.component, action_id:hash, action:table)|nil Called when input event is consumed before +---@field on_message fun(self:druid.component, message_id:hash, message:table, sender:url)|nil Called when message is received +---@field on_late_init fun(self:druid.component)|nil Called before update once time after GUI init +---@field on_focus_lost fun(self:druid.component)|nil Called when app lost focus +---@field on_focus_gained fun(self:druid.component)|nil Called when app gained focus +---@field on_style_change fun(self:druid.component, style: table)|nil Called when style is changed +---@field on_layout_change fun(self:druid.component)|nil Called when GUI layout is changed +---@field on_window_resized fun(self:druid.component)|nil Called when window is resized +---@field on_language_change fun(self:druid.component)|nil Called when language is changed ---@field private _component druid.component.component ---@field private _meta druid.component.meta local M = {} diff --git a/druid/system/druid_instance.lua b/druid/system/druid_instance.lua index ddf5252..348c112 100755 --- a/druid/system/druid_instance.lua +++ b/druid/system/druid_instance.lua @@ -166,6 +166,11 @@ function M:_can_use_input_component(component) end +---Process input for components +---@param action_id hash Action_id from on_input +---@param action table Action from on_input +---@param components druid.component[] Components to process input +---@return boolean The boolean value is input was consumed function M:_process_input(action_id, action, components) local is_input_consumed = false @@ -313,9 +318,9 @@ function M:remove(component) end ---- Druid late update function called after initialization and before the regular update step --- This function is used to check the GUI state and perform actions after all components and nodes have been created. --- An example use case is performing an auto stencil check in the GUI hierarchy for input components. +---Druid late update function called after initialization and before the regular update step. +---This function is used to check the GUI state and perform actions after all components and nodes have been created. +---An example use case is performing an auto stencil check in the GUI hierarchy for input components. ---@private function M:late_init() local late_init_components = self.components_interest[const.ON_LATE_INIT] @@ -457,7 +462,7 @@ function M:set_blacklist(blacklist_components) end ---- Remove all components on late remove step DruidInstance +---Remove all components on late remove step DruidInstance ---@private function M:_clear_late_remove() if #self._late_remove == 0 then diff --git a/example/examples/data_list/examples_list.lua b/example/examples/data_list/examples_list.lua index 1f496fa..22329f5 100644 --- a/example/examples/data_list/examples_list.lua +++ b/example/examples/data_list/examples_list.lua @@ -200,4 +200,4 @@ function M.get_examples() } end -return M \ No newline at end of file +return M diff --git a/example/examples/widgets/fps_panel/example_fps_panel.lua b/example/examples/widgets/fps_panel/example_fps_panel.lua index 50de100..9b460b7 100644 --- a/example/examples/widgets/fps_panel/example_fps_panel.lua +++ b/example/examples/widgets/fps_panel/example_fps_panel.lua @@ -9,4 +9,4 @@ function M:init() end -return M \ No newline at end of file +return M