From 66f671c145693cb9e043d469b45cccc43fead3a6 Mon Sep 17 00:00:00 2001 From: NaakkaDev Date: Mon, 29 Sep 2025 20:14:14 +0300 Subject: [PATCH 1/5] Added navigation handler for gamepad/keyboard navigation. --- druid/base/navigation_handler.lua | 391 ++++++++++++++++++++++++++++++ druid/const.lua | 3 + druid/ext.properties | 6 + druid/system/druid_instance.lua | 44 +--- 4 files changed, 408 insertions(+), 36 deletions(-) create mode 100644 druid/base/navigation_handler.lua diff --git a/druid/base/navigation_handler.lua b/druid/base/navigation_handler.lua new file mode 100644 index 0000000..ec8f832 --- /dev/null +++ b/druid/base/navigation_handler.lua @@ -0,0 +1,391 @@ +local event = require("event.event") +local const = require("druid.const") +local component = require("druid.component") + + +---Navigation handler style params. +---You can override this component styles params in Druid styles table or create your own style +---@class druid.navigation_handler.style +---@field on_select fun(self, node, hover_state)|nil Currently only used for when a slider component is selected. For buttons use its own on_hover style. + + +---Component to handle GUI navigation via keyboard/gamepad. +--- +---### Setup +---Create navigation handler component with druid: `druid:new_navigation_handler(button)` +--- +---### Notes +---- Key triggers in `input.binding` should match your setup +---- Used `action_id`'s are:' key_up, key_down, key_left and key_right +---@class druid.navigation_handler: druid.component +---@field COMPONENTS table Table of component names navigation handler can handle. +---@field on_select event fun(self, button_instance, button_instance) Triggers when a new button is selected. The first button_instance is for the newly selected and the second for the previous button. +---@field private _weight number The value used to control of the next button diagonal finding logic strictness. +---@field private _tolerance number Determines how lenient the next button finding logic is. Set larger value for further diagonal navigation. +---@field private _select_trigger hash Select trigger for the current component. Defaults to `druid.const.ACTION_SPACE`. +---@field private _selected_triggers table Table of action_ids that can trigger the selected component. Valid only for the current button when set. +---@field private _selected_component druid.component|druid.button|druid.slider Currently selected button instance. +---@field private _deselect_directions table The valid "escape" direction of the current selection. +local M = component.create("navigation_handler") + + +M.COMPONENTS = { "button", "slider" } + + +---@private +---@param style druid.navigation_handler.style +function M:on_style_change(style) + self.style = { + on_select = style.on_select or function(_, node, state) end, + } +end + +---The constructor for the navigation_handler component. +---@param component druid.component Current druid component that starts as selected. +---@param tolerance number|nil How far to allow misalignment on the perpendicular axis when finding the next component. +function M:init(component, tolerance) + -- Set default tolerance if not given. + if tolerance == nil then + tolerance = 200 + end + + self._weight = 10 + self._tolerance = tolerance + self._select_trigger = const.ACTION_SPACE + self._selected_triggers = {} + self._selected_component = component + self._deselect_directions = {} + + -- Select the component if it's a button. + if component.hover then + component.hover:set_hover(true) + end + + -- Events + self.on_select = event.create() + + -- Set style for the initial component. + self.style.on_select(self, component.node, true) +end + +---@private +---@param action_id hash Action id from on_input. +---@param action table Action from on_input. +---@return boolean is_consumed True if the input was consumed. +function M:on_input(action_id, action) + -- Trigger an action with the selected component, e.g. button click. + if self:_action_id_is_trigger(action_id) and self:_selected_is_button() then + ---@type druid.button + local btn = self._selected_component + local is_consume = false + + if action.pressed then + btn.is_repeated_started = false + btn.last_pressed_time = socket.gettime() + btn.on_pressed:trigger(self:get_context(), btn, self) + btn.can_action = true + return is_consume + end + + -- While hold button, repeat rate pick from input.repeat_interval + if action.repeated then + if not btn.on_repeated_click:is_empty() and btn.can_action then + btn:_on_button_repeated_click() + return is_consume + end + end + + if action.released then + return btn:_on_button_release() and is_consume + end + + return not btn.disabled and is_consume + end + + if action.pressed then + ---@type druid.component|nil + local component = nil + + if action_id == const.ACTION_UP then + component = self:_find_next_button("up") + elseif action_id == const.ACTION_DOWN then + component = self:_find_next_button("down") + elseif action_id == const.ACTION_LEFT then + component = self:_find_next_button("left") + elseif action_id == const.ACTION_RIGHT then + component = self:_find_next_button("right") + end + + if component ~= nil and component ~= self._selected_component then + return self:_on_new_select(component) + end + end + + -- Handle chaning slider values when pressing left or right keys. + if (action.pressed or action.repeated) + and self:_selected_is_slider() + and (action_id == const.ACTION_LEFT or action_id == const.ACTION_RIGHT) + then + ---@type druid.slider + local slider = self._selected_component + local value = slider.value + local new_value = 0.01 + + -- Speedup when holding the button. + if action.repeated and not action.pressed then + new_value = 0.05 + end + + if action_id == const.ACTION_LEFT then + -- Decrease value. + value = value - new_value + elseif action_id == const.ACTION_RIGHT then + -- Increase value. + value = value + new_value + end + + slider:set(value) + end + + return false +end + +---Sets a new weight value which affects the next button diagonal finding logic. +---@param new_value number +---@return druid.navigation_handler +function M:set_weight(new_value) + self._weight = new_value + return self +end + +---Sets a new tolerance value. Can be useful when scale or window size changes. +---@param new_value number How far to allow misalignment on the perpendicular axis when finding the next button. +---@return druid.navigation_handler self The current navigation handler instance. +function M:set_tolerance(new_value) + self._tolerance = new_value + return self +end + +---Set input action_id name to trigger selected component by keyboard/gamepad. +---@param key hash|string The action_id of the input key. Example: "key_space". +---@return druid.navigation_handler self The current navigation handler instance. +function M:set_select_trigger(key) + if type(key) == "string" then + self._select_trigger = hash(key) + else + self._select_trigger = key + end + + return self +end + +---Get current the trigger key for currently selected component. +---@return hash _select_trigger The action_id of the input key. +function M:get_select_trigger() + return self._select_trigger +end + +---Set the trigger keys for the selected component. Stays valid until the selected component changes. +---@param keys table|string|hash Supports multiple action_ids if the given value is a table with the action_id hashes or strings. +---@return druid.navigation_handler self The current navigation handler instance. +function M:set_temporary_select_triggers(keys) + if type(keys) == "table" then + for index, value in ipairs(keys) do + if type(value) == "string" then + keys[index] = hash(value) + end + end + self._selected_triggers = keys + elseif type(keys) == "string" then + self._selected_triggers = { hash(keys) } + else + self._selected_triggers = { keys } + end + + return self +end + +---Get the currently selected component. +---@return druid.component _selected_component Selected component, which often is a `druid.button`. +function M:get_selected_component() + return self._selected_component +end + +---Set the de-select direction for the selected button. If this is set +---then the next button can only be in that direction. +---@param dir string|table Valid directions: "up", "down", "left", "right". Can take multiple values as a table of strings. +---@return druid.navigation_handler self The current navigation handler instance. +function M:set_deselect_directions(dir) + if type(dir) == "table" then + self._deselect_directions = dir + elseif type(dir) == "string" then + self._deselect_directions = { dir } + end + + return self +end + +---Returns true if the currently selected `druid.component` is a `druid.button`. +---@private +---@return boolean +function M:_selected_is_button() + return self._selected_component._component.name == "button" +end + +---Returns true if the currently selected `druid.component` is a `druid.slider`. +---@private +---@return boolean +function M:_selected_is_slider() + return self._selected_component._component.name == "slider" +end + +---Find the best next button based on the direction from the currently selected button. +---@private +---@param dir string Valid directions: "top", "bottom", "left", "right". +---@return druid.component|nil +function M:_find_next_button(dir) + ---Helper method for checking if the given direction is valid. + ---@param dirs table + ---@param dir string + ---@return boolean + local function valid_direction(dirs, dir) + for _index, value in ipairs(dirs) do + if value == dir then + return true + end + end + return false + end + + ---Helper method for checking iterating through components. + ---Returns true if the given component is in the table of valid components. + ---@param input_component druid.component + ---@return boolean + local function valid_component(input_component) + local component_name = input_component._component.name + for _index, component in ipairs(M.COMPONENTS) do + if component_name == component then + return true + end + end + return false + end + + -- Check if the deselect direction is set and + -- the direction is different from it. + if next(self._deselect_directions) ~= nil and not valid_direction(self._deselect_directions, dir) then + return nil + end + + local best_component, best_score = nil, math.huge + local screen_pos = gui.get_screen_position(self._selected_component.node) + + -- Use the slider parent node instead of the pin node. + if self._selected_component._component.name == "slider" then + screen_pos = gui.get_screen_position(gui.get_parent(self._selected_component.node)) + end + + ---@type druid.component + for _, input_component in ipairs(self._meta.druid.components_interest[const.ON_INPUT]) do + -- GUI node of the component being iterated. + local node = input_component.node + + -- If it is a slider component then use its parent node instead, + -- since the pin node moves around. + if input_component._component.name == "slider" then + node = gui.get_parent(node) + end + + -- Only check components that are supported. + if input_component ~= self._selected_component and valid_component(input_component) then + local pos = gui.get_screen_position(node) + local dx, dy = pos.x - screen_pos.x, pos.y - screen_pos.y + local valid = false + local score = math.huge + + if dir == "right" and dx > 0 and math.abs(dy) <= self._tolerance then + valid = true + score = dx * dx + dy * dy * self._weight + elseif dir == "left" and dx < 0 and math.abs(dy) <= self._tolerance then + valid = true + score = dx * dx + dy * dy * self._weight + elseif dir == "up" and dy > 0 and math.abs(dx) <= self._tolerance then + valid = true + score = dy * dy + dx * dx * self._weight + elseif dir == "down" and dy < 0 and math.abs(dx) <= self._tolerance then + valid = true + score = dy * dy + dx * dx * self._weight + end + + if valid and score < best_score then + best_score = score + best_component = input_component + end + end + end + + return best_component +end + +---De-select the current selected component. +---@private +function M:_deselect_current() + if self._selected_component.hover then + self._selected_component.hover:set_hover(false) + end + self._selected_component = nil + self._selected_triggers = {} + + -- The deselect direction was used so remove it. + if self._deselect_directions then + self._deselect_directions = {} + end +end + +---Check if the supplied action_id can trigger the selected component. +---@private +---@param action_id hash +---@return boolean +function M:_action_id_is_trigger(action_id) + for _, key in ipairs(self._selected_triggers) do + if action_id == key then + return true + end + end + + return action_id == self._select_trigger +end + +---Handle new selection. +---@private +---@param new druid.component Instance of the selected component. +---@return boolean +function M:_on_new_select(new) + ---@type druid.component + local current = self._selected_component + + self.style.on_select(self, current.node, false) + self.style.on_select(self, new.node, true) + + -- De-select the current component. + self:_deselect_current() + self._selected_component = new + + --- BUTTON + if new._component.name == "button" then + -- Set the active button hover state. + new.hover:set_hover(true) + end + + --- SLIDER + if new._component.name == "slider" then + self:set_deselect_directions({ "up", "down" }) + end + + --- EVENT + self.on_select:trigger(new, current) + + return false +end + +return M diff --git a/druid/const.lua b/druid/const.lua index 0363871..a59d5a0 100755 --- a/druid/const.lua +++ b/druid/const.lua @@ -7,12 +7,15 @@ M.ACTION_MARKED_TEXT = hash(sys.get_config_string("druid.input_marked_text", "ma M.ACTION_ESC = hash(sys.get_config_string("druid.input_key_esc", "key_esc")) M.ACTION_BACK = hash(sys.get_config_string("druid.input_key_back", "key_back")) M.ACTION_ENTER = hash(sys.get_config_string("druid.input_key_enter", "key_enter")) +M.ACTION_SPACE = hash(sys.get_config_string("druid.input_key_space", "key_space")) M.ACTION_MULTITOUCH = hash(sys.get_config_string("druid.input_multitouch", "touch_multi")) M.ACTION_BACKSPACE = hash(sys.get_config_string("druid.input_key_backspace", "key_backspace")) M.ACTION_SCROLL_UP = hash(sys.get_config_string("druid.input_scroll_up", "mouse_wheel_up")) M.ACTION_SCROLL_DOWN = hash(sys.get_config_string("druid.input_scroll_down", "mouse_wheel_down")) M.ACTION_LEFT = hash(sys.get_config_string("druid.input_key_left", "key_left")) M.ACTION_RIGHT = hash(sys.get_config_string("druid.input_key_right", "key_right")) +M.ACTION_UP = hash(sys.get_config_string("druid.input_key_up", "key_up")) +M.ACTION_DOWN = hash(sys.get_config_string("druid.input_key_down", "key_down")) M.ACTION_LSHIFT = hash(sys.get_config_string("druid.input_key_lshift", "key_lshift")) M.ACTION_LCTRL = hash(sys.get_config_string("druid.input_key_lctrl", "key_lctrl")) M.ACTION_LCMD = hash(sys.get_config_string("druid.input_key_lsuper", "key_lsuper")) diff --git a/druid/ext.properties b/druid/ext.properties index 09f44dd..ec51e70 100644 --- a/druid/ext.properties +++ b/druid/ext.properties @@ -14,6 +14,8 @@ input_key_back.default = key_back input_key_enter.default = key_enter +input_key_space.default = key_space + input_key_backspace.default = key_backspace input_multitouch.default = touch_multi @@ -26,6 +28,10 @@ input_key_left.default = key_left input_key_right.default = key_right +input_key_up.default = key_up + +input_key_down.default = key_down + input_key_lshift.default = key_lshift input_key_lctrl.default = key_lctrl diff --git a/druid/system/druid_instance.lua b/druid/system/druid_instance.lua index 08c8f33..6c33ccd 100755 --- a/druid/system/druid_instance.lua +++ b/druid/system/druid_instance.lua @@ -163,7 +163,6 @@ function M:_can_use_input_component(component) return can_by_blacklist and can_by_whitelist end - local function schedule_late_init(self) if self._late_init_timer_id then return @@ -203,7 +202,6 @@ function M.create_druid_instance(context, style) return self end - ---Create new Druid component instance ---@generic T: druid.component ---@param component T The component class to create @@ -223,7 +221,6 @@ function M:new(component, ...) return instance end - ---Call this in gui_script final function. function M:final() local components = self.components_all @@ -240,7 +237,6 @@ function M:final() events.unsubscribe("druid.language_change", self.on_language_change, self) end - ---Remove created component from Druid instance. --- ---Component `on_remove` function will be invoked, if exist. @@ -292,7 +288,6 @@ function M:remove(component) return is_removed end - ---Get a context of Druid instance (usually a self of gui script) ---@package ---@return any context The Druid context @@ -300,7 +295,6 @@ function M:get_context() return self._context end - ---Get a style of Druid instance ---@package ---@return table style The Druid style table @@ -308,7 +302,6 @@ function M:get_style() return self._style 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. @@ -326,7 +319,6 @@ function M:late_init() end end - ---Call this in gui_script update function. ---@param dt number Delta time function M:update(dt) @@ -341,7 +333,6 @@ function M:update(dt) self:_clear_late_remove() end - ---Call this in gui_script on_input function. ---@param action_id hash Action_id from on_input ---@param action table Action from on_input @@ -375,7 +366,6 @@ function M:on_input(action_id, action) return is_input_consumed end - ---Call this in gui_script on_message function. ---@param message_id hash Message_id from on_message ---@param message table Message from on_message @@ -396,7 +386,6 @@ function M:on_message(message_id, message, sender) end end - ---Called when the window event occurs ---@param window_event number The window event function M:on_window_event(window_event) @@ -418,7 +407,6 @@ function M:on_window_event(window_event) end end - ---Calls the on_language_change function in all related components ---This one called by global druid.on_language_change, but can be called manually to update all translations ---@private @@ -429,7 +417,6 @@ function M:on_language_change() end end - ---Set whitelist components for input processing. ---If whitelist is not empty and component not contains in this list, ---component will be not processed on the input step @@ -449,7 +436,6 @@ function M:set_whitelist(whitelist_components) return self end - ---Set blacklist components for input processing. ---If blacklist is not empty and component is contained in this list, ---component will be not processed on the input step DruidInstance @@ -469,7 +455,6 @@ function M:set_blacklist(blacklist_components) return self end - ---Remove all components on late remove step DruidInstance ---@private function M:_clear_late_remove() @@ -483,7 +468,6 @@ function M:_clear_late_remove() self._late_remove = {} end - ---Create new Druid widget instance ---@generic T: druid.component ---@param widget T The widget class to create @@ -506,7 +490,6 @@ function M:new_widget(widget, template, nodes, ...) return instance end - local button = require("druid.base.button") ---Create Button component ---@param node string|node The node_id or gui.get_node(node_id) @@ -518,7 +501,6 @@ function M:new_button(node, callback, params, anim_node) return self:new(button, node, callback, params, anim_node) end - local blocker = require("druid.base.blocker") ---Create Blocker component ---@param node string|node The node_id or gui.get_node(node_id) @@ -527,7 +509,6 @@ function M:new_blocker(node) return self:new(blocker, node) end - local back_handler = require("druid.base.back_handler") ---Create BackHandler component ---@param callback function|event|nil The callback(self, custom_args) to call on back event @@ -537,7 +518,6 @@ function M:new_back_handler(callback, params) return self:new(back_handler, callback, params) end - local hover = require("druid.base.hover") ---Create Hover component ---@param node string|node The node_id or gui.get_node(node_id) @@ -548,6 +528,14 @@ function M:new_hover(node, on_hover_callback, on_mouse_hover_callback) return self:new(hover, node, on_hover_callback, on_mouse_hover_callback) end +local navigation_handler = require("druid.base.navigation_handler") +---Create NavigationHandler component +---@param button druid.button The button that should be selected on start. +---@param tolerance number|nil How far to allow misalignment on the perpendicular axis when finding the next button. +---@return druid.navigation_handler navigation_handler The new navigation handler component. +function M:new_navigation_handler(button, tolerance) + return self:new(navigation_handler, button, tolerance) +end local text = require("druid.base.text") ---Create Text component @@ -559,7 +547,6 @@ function M:new_text(node, value, adjust_type) return self:new(text, node, value, adjust_type) end - local static_grid = require("druid.base.static_grid") ---Create Grid component ---@param parent_node string|node The node_id or gui.get_node(node_id). Parent of all Grid items. @@ -570,7 +557,6 @@ function M:new_grid(parent_node, item, in_row) return self:new(static_grid, parent_node, item, in_row) end - local scroll = require("druid.base.scroll") ---Create Scroll component ---@param view_node string|node The node_id or gui.get_node(node_id). Will be used as user input node. @@ -580,7 +566,6 @@ function M:new_scroll(view_node, content_node) return self:new(scroll, view_node, content_node) end - local drag = require("druid.base.drag") ---Create Drag component ---@param node string|node The node_id or gui.get_node(node_id). Will be used as user input node. @@ -590,7 +575,6 @@ function M:new_drag(node, on_drag_callback) return self:new(drag, node, on_drag_callback) end - local swipe = require("druid.extended.swipe") ---Create Swipe component ---@param node string|node The node_id or gui.get_node(node_id). Will be used as user input node. @@ -600,7 +584,6 @@ function M:new_swipe(node, on_swipe_callback) return self:new(swipe, node, on_swipe_callback) end - local lang_text = require("druid.extended.lang_text") ---Create LangText component ---@param node string|node The node_id or gui.get_node(node_id) @@ -611,7 +594,6 @@ function M:new_lang_text(node, locale_id, adjust_type) return self:new(lang_text, node, locale_id, adjust_type) end - local slider = require("druid.extended.slider") ---Create Slider component ---@param pin_node string|node The node_id or gui.get_node(node_id). @@ -622,7 +604,6 @@ function M:new_slider(pin_node, end_pos, callback) return self:new(slider, pin_node, end_pos, callback) end - local input = require("druid.extended.input") ---Create Input component ---@param click_node string|node Button node to enable input component @@ -633,7 +614,6 @@ function M:new_input(click_node, text_node, keyboard_type) return self:new(input, click_node, text_node, keyboard_type) end - local data_list = require("druid.extended.data_list") ---Create DataList component ---@param druid_scroll druid.scroll The Scroll instance for Data List component @@ -644,7 +624,6 @@ function M:new_data_list(druid_scroll, druid_grid, create_function) return self:new(data_list, druid_scroll, druid_grid, create_function) end - local timer_component = require("druid.extended.timer") ---Create Timer component ---@param node string|node Gui text node @@ -656,7 +635,6 @@ function M:new_timer(node, seconds_from, seconds_to, callback) return self:new(timer_component, node, seconds_from, seconds_to, callback) end - local progress = require("druid.extended.progress") ---Create Progress component ---@param node string|node Progress bar fill node or node name @@ -667,7 +645,6 @@ function M:new_progress(node, key, init_value) return self:new(progress, node, key, init_value) end - local layout = require("druid.extended.layout") ---Create Layout component ---@param node string|node The node_id or gui.get_node(node_id). @@ -677,7 +654,6 @@ function M:new_layout(node, mode) return self:new(layout, node, mode) end - local container = require("druid.extended.container") ---Create Container component ---@param node string|node The node_id or gui.get_node(node_id). @@ -688,7 +664,6 @@ function M:new_container(node, mode, callback) return self:new(container, node, mode, callback) end - local hotkey = require("druid.extended.hotkey") ---Create Hotkey component ---@param keys_array string|string[] Keys for trigger action. Should contains one action key and any amount of modificator keys @@ -699,7 +674,6 @@ function M:new_hotkey(keys_array, callback, callback_argument) return self:new(hotkey, keys_array, callback, callback_argument) end - local rich_text = require("druid.custom.rich_text.rich_text") ---Create RichText component. ---@param text_node string|node The text node to make Rich Text @@ -709,7 +683,6 @@ function M:new_rich_text(text_node, value) return self:new(rich_text, text_node, value) end - local rich_input = require("druid.custom.rich_input.rich_input") ---Create RichInput component. ---As a template please check rich_input.gui layout. @@ -720,5 +693,4 @@ function M:new_rich_input(template, nodes) return self:new(rich_input, template, nodes) end - return M From 7fd91d1f0afb498559b645df4456aebd16cbe702 Mon Sep 17 00:00:00 2001 From: NaakkaDev Date: Tue, 30 Sep 2025 10:34:17 +0300 Subject: [PATCH 2/5] Take in account that sliders can be vertical too. --- druid/base/navigation_handler.lua | 49 ++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/druid/base/navigation_handler.lua b/druid/base/navigation_handler.lua index ec8f832..cb91788 100644 --- a/druid/base/navigation_handler.lua +++ b/druid/base/navigation_handler.lua @@ -102,17 +102,22 @@ function M:on_input(action_id, action) return not btn.disabled and is_consume end + local is_left = action_id == const.ACTION_LEFT + local is_right = action_id == const.ACTION_RIGHT + local is_up = action_id == const.ACTION_UP + local is_down = action_id == const.ACTION_DOWN + if action.pressed then ---@type druid.component|nil local component = nil - if action_id == const.ACTION_UP then + if is_up then component = self:_find_next_button("up") - elseif action_id == const.ACTION_DOWN then + elseif is_down then component = self:_find_next_button("down") - elseif action_id == const.ACTION_LEFT then + elseif is_left then component = self:_find_next_button("left") - elseif action_id == const.ACTION_RIGHT then + elseif is_right then component = self:_find_next_button("right") end @@ -124,23 +129,37 @@ function M:on_input(action_id, action) -- Handle chaning slider values when pressing left or right keys. if (action.pressed or action.repeated) and self:_selected_is_slider() - and (action_id == const.ACTION_LEFT or action_id == const.ACTION_RIGHT) then + local is_directional = is_left or is_right or is_up or is_down + + -- The action_id was not one of the directions so go no further. + if not is_directional then + return false + end + ---@type druid.slider local slider = self._selected_component local value = slider.value local new_value = 0.01 + local is_horizontal = slider.dist.x > 0 + local negative_value = is_left or is_down + local positive_value = is_right or is_up + + -- Reteurn if a navigation should happen instead of a value change. + if is_horizontal and (is_up or is_down) then + return false + elseif not is_horizontal and (is_left or is_right) then + return false + end -- Speedup when holding the button. if action.repeated and not action.pressed then new_value = 0.05 end - if action_id == const.ACTION_LEFT then - -- Decrease value. + if negative_value then value = value - new_value - elseif action_id == const.ACTION_RIGHT then - -- Increase value. + elseif positive_value then value = value + new_value end @@ -379,7 +398,17 @@ function M:_on_new_select(new) --- SLIDER if new._component.name == "slider" then - self:set_deselect_directions({ "up", "down" }) + -- Check if the slider is horizontal, if so then + -- the next component should be above or below of this one. + if new.dist.x > 0 then + self:set_deselect_directions({ "up", "down" }) + end + + -- Check if the slider is vertical, if so then + -- the next component should be left or right of this one. + if new.dist.y > 0 then + self:set_deselect_directions({ "left, right" }) + end end --- EVENT From 9e51fa651e32563854811a0660b7794942ec2927 Mon Sep 17 00:00:00 2001 From: NaakkaDev Date: Sat, 4 Oct 2025 10:29:14 +0300 Subject: [PATCH 3/5] Removed changes from druid_instance.lua. Moved navigation handler to widgets instead. Updated advanced-setup.md with the added input keys. Adjusted the comments in navigation handler module. --- druid/system/druid_instance.lua | 44 ++++++-- .../navigation_handler.lua | 100 +++++++++++------- wiki/advanced-setup.md | 6 ++ 3 files changed, 103 insertions(+), 47 deletions(-) rename druid/{base => widget/navigation_handler}/navigation_handler.lua (85%) diff --git a/druid/system/druid_instance.lua b/druid/system/druid_instance.lua index 6c33ccd..08c8f33 100755 --- a/druid/system/druid_instance.lua +++ b/druid/system/druid_instance.lua @@ -163,6 +163,7 @@ function M:_can_use_input_component(component) return can_by_blacklist and can_by_whitelist end + local function schedule_late_init(self) if self._late_init_timer_id then return @@ -202,6 +203,7 @@ function M.create_druid_instance(context, style) return self end + ---Create new Druid component instance ---@generic T: druid.component ---@param component T The component class to create @@ -221,6 +223,7 @@ function M:new(component, ...) return instance end + ---Call this in gui_script final function. function M:final() local components = self.components_all @@ -237,6 +240,7 @@ function M:final() events.unsubscribe("druid.language_change", self.on_language_change, self) end + ---Remove created component from Druid instance. --- ---Component `on_remove` function will be invoked, if exist. @@ -288,6 +292,7 @@ function M:remove(component) return is_removed end + ---Get a context of Druid instance (usually a self of gui script) ---@package ---@return any context The Druid context @@ -295,6 +300,7 @@ function M:get_context() return self._context end + ---Get a style of Druid instance ---@package ---@return table style The Druid style table @@ -302,6 +308,7 @@ function M:get_style() return self._style 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. @@ -319,6 +326,7 @@ function M:late_init() end end + ---Call this in gui_script update function. ---@param dt number Delta time function M:update(dt) @@ -333,6 +341,7 @@ function M:update(dt) self:_clear_late_remove() end + ---Call this in gui_script on_input function. ---@param action_id hash Action_id from on_input ---@param action table Action from on_input @@ -366,6 +375,7 @@ function M:on_input(action_id, action) return is_input_consumed end + ---Call this in gui_script on_message function. ---@param message_id hash Message_id from on_message ---@param message table Message from on_message @@ -386,6 +396,7 @@ function M:on_message(message_id, message, sender) end end + ---Called when the window event occurs ---@param window_event number The window event function M:on_window_event(window_event) @@ -407,6 +418,7 @@ function M:on_window_event(window_event) end end + ---Calls the on_language_change function in all related components ---This one called by global druid.on_language_change, but can be called manually to update all translations ---@private @@ -417,6 +429,7 @@ function M:on_language_change() end end + ---Set whitelist components for input processing. ---If whitelist is not empty and component not contains in this list, ---component will be not processed on the input step @@ -436,6 +449,7 @@ function M:set_whitelist(whitelist_components) return self end + ---Set blacklist components for input processing. ---If blacklist is not empty and component is contained in this list, ---component will be not processed on the input step DruidInstance @@ -455,6 +469,7 @@ function M:set_blacklist(blacklist_components) return self end + ---Remove all components on late remove step DruidInstance ---@private function M:_clear_late_remove() @@ -468,6 +483,7 @@ function M:_clear_late_remove() self._late_remove = {} end + ---Create new Druid widget instance ---@generic T: druid.component ---@param widget T The widget class to create @@ -490,6 +506,7 @@ function M:new_widget(widget, template, nodes, ...) return instance end + local button = require("druid.base.button") ---Create Button component ---@param node string|node The node_id or gui.get_node(node_id) @@ -501,6 +518,7 @@ function M:new_button(node, callback, params, anim_node) return self:new(button, node, callback, params, anim_node) end + local blocker = require("druid.base.blocker") ---Create Blocker component ---@param node string|node The node_id or gui.get_node(node_id) @@ -509,6 +527,7 @@ function M:new_blocker(node) return self:new(blocker, node) end + local back_handler = require("druid.base.back_handler") ---Create BackHandler component ---@param callback function|event|nil The callback(self, custom_args) to call on back event @@ -518,6 +537,7 @@ function M:new_back_handler(callback, params) return self:new(back_handler, callback, params) end + local hover = require("druid.base.hover") ---Create Hover component ---@param node string|node The node_id or gui.get_node(node_id) @@ -528,14 +548,6 @@ function M:new_hover(node, on_hover_callback, on_mouse_hover_callback) return self:new(hover, node, on_hover_callback, on_mouse_hover_callback) end -local navigation_handler = require("druid.base.navigation_handler") ----Create NavigationHandler component ----@param button druid.button The button that should be selected on start. ----@param tolerance number|nil How far to allow misalignment on the perpendicular axis when finding the next button. ----@return druid.navigation_handler navigation_handler The new navigation handler component. -function M:new_navigation_handler(button, tolerance) - return self:new(navigation_handler, button, tolerance) -end local text = require("druid.base.text") ---Create Text component @@ -547,6 +559,7 @@ function M:new_text(node, value, adjust_type) return self:new(text, node, value, adjust_type) end + local static_grid = require("druid.base.static_grid") ---Create Grid component ---@param parent_node string|node The node_id or gui.get_node(node_id). Parent of all Grid items. @@ -557,6 +570,7 @@ function M:new_grid(parent_node, item, in_row) return self:new(static_grid, parent_node, item, in_row) end + local scroll = require("druid.base.scroll") ---Create Scroll component ---@param view_node string|node The node_id or gui.get_node(node_id). Will be used as user input node. @@ -566,6 +580,7 @@ function M:new_scroll(view_node, content_node) return self:new(scroll, view_node, content_node) end + local drag = require("druid.base.drag") ---Create Drag component ---@param node string|node The node_id or gui.get_node(node_id). Will be used as user input node. @@ -575,6 +590,7 @@ function M:new_drag(node, on_drag_callback) return self:new(drag, node, on_drag_callback) end + local swipe = require("druid.extended.swipe") ---Create Swipe component ---@param node string|node The node_id or gui.get_node(node_id). Will be used as user input node. @@ -584,6 +600,7 @@ function M:new_swipe(node, on_swipe_callback) return self:new(swipe, node, on_swipe_callback) end + local lang_text = require("druid.extended.lang_text") ---Create LangText component ---@param node string|node The node_id or gui.get_node(node_id) @@ -594,6 +611,7 @@ function M:new_lang_text(node, locale_id, adjust_type) return self:new(lang_text, node, locale_id, adjust_type) end + local slider = require("druid.extended.slider") ---Create Slider component ---@param pin_node string|node The node_id or gui.get_node(node_id). @@ -604,6 +622,7 @@ function M:new_slider(pin_node, end_pos, callback) return self:new(slider, pin_node, end_pos, callback) end + local input = require("druid.extended.input") ---Create Input component ---@param click_node string|node Button node to enable input component @@ -614,6 +633,7 @@ function M:new_input(click_node, text_node, keyboard_type) return self:new(input, click_node, text_node, keyboard_type) end + local data_list = require("druid.extended.data_list") ---Create DataList component ---@param druid_scroll druid.scroll The Scroll instance for Data List component @@ -624,6 +644,7 @@ function M:new_data_list(druid_scroll, druid_grid, create_function) return self:new(data_list, druid_scroll, druid_grid, create_function) end + local timer_component = require("druid.extended.timer") ---Create Timer component ---@param node string|node Gui text node @@ -635,6 +656,7 @@ function M:new_timer(node, seconds_from, seconds_to, callback) return self:new(timer_component, node, seconds_from, seconds_to, callback) end + local progress = require("druid.extended.progress") ---Create Progress component ---@param node string|node Progress bar fill node or node name @@ -645,6 +667,7 @@ function M:new_progress(node, key, init_value) return self:new(progress, node, key, init_value) end + local layout = require("druid.extended.layout") ---Create Layout component ---@param node string|node The node_id or gui.get_node(node_id). @@ -654,6 +677,7 @@ function M:new_layout(node, mode) return self:new(layout, node, mode) end + local container = require("druid.extended.container") ---Create Container component ---@param node string|node The node_id or gui.get_node(node_id). @@ -664,6 +688,7 @@ function M:new_container(node, mode, callback) return self:new(container, node, mode, callback) end + local hotkey = require("druid.extended.hotkey") ---Create Hotkey component ---@param keys_array string|string[] Keys for trigger action. Should contains one action key and any amount of modificator keys @@ -674,6 +699,7 @@ function M:new_hotkey(keys_array, callback, callback_argument) return self:new(hotkey, keys_array, callback, callback_argument) end + local rich_text = require("druid.custom.rich_text.rich_text") ---Create RichText component. ---@param text_node string|node The text node to make Rich Text @@ -683,6 +709,7 @@ function M:new_rich_text(text_node, value) return self:new(rich_text, text_node, value) end + local rich_input = require("druid.custom.rich_input.rich_input") ---Create RichInput component. ---As a template please check rich_input.gui layout. @@ -693,4 +720,5 @@ function M:new_rich_input(template, nodes) return self:new(rich_input, template, nodes) end + return M diff --git a/druid/base/navigation_handler.lua b/druid/widget/navigation_handler/navigation_handler.lua similarity index 85% rename from druid/base/navigation_handler.lua rename to druid/widget/navigation_handler/navigation_handler.lua index cb91788..6fc40f4 100644 --- a/druid/base/navigation_handler.lua +++ b/druid/widget/navigation_handler/navigation_handler.lua @@ -1,44 +1,55 @@ +-- Title: Navigation Handler +-- Description: Adds support for navigating with keyboard and gamepad. +-- Author: NaakkaDev +-- Widget: navigation_handler +-- Tags: input +-- Version: 1 + + local event = require("event.event") local const = require("druid.const") -local component = require("druid.component") ----Navigation handler style params. ----You can override this component styles params in Druid styles table or create your own style ----@class druid.navigation_handler.style ----@field on_select fun(self, node, hover_state)|nil Currently only used for when a slider component is selected. For buttons use its own on_hover style. - - ----Component to handle GUI navigation via keyboard/gamepad. +---Widget force handling GUI navigation via keyboard/gamepad. --- ---### Setup ----Create navigation handler component with druid: `druid:new_navigation_handler(button)` +---Loads the widget module: +---`local navigation_handler = require("druid.widgets.navigation_handler.navigation_handler")` +--- +---Create the new widget instance: +---`self.nav = self.druid:new_widget(navigation_handler, self.my_button)` +--- +--- +---### Example using the `on_select` event +---``` +---local function on_select_btn(self, new, current) +--- gui.play_flipbook(new.node, "button_selected") +--- gui.play_flipbook(current.node, "button") +---end +---``` +---With `self.nav.on_select:subscribe(on_select_btn)` +--- --- ---### Notes ----- Key triggers in `input.binding` should match your setup ----- Used `action_id`'s are:' key_up, key_down, key_left and key_right ----@class druid.navigation_handler: druid.component ----@field COMPONENTS table Table of component names navigation handler can handle. ----@field on_select event fun(self, button_instance, button_instance) Triggers when a new button is selected. The first button_instance is for the newly selected and the second for the previous button. +---- `on_select` event callback params: (self, component_instance, component_instance). +---- - **self** - Druid self context. +---- - **new** - The component that will be selected next. +---- - **current** - The component that is about to be de-selected. +---- Key triggers in `input.binding` should match your setup. +---- Used `action_id`'s are:' `key_up`, `key_down`, `key_left` and `key_right`. +---@class druid.widget.navigation_handler: druid.widget +---@field on_select event fun(self, component_instance, component_instance) Triggers when a new component is selected. The first component is for the newly selected and the second is for the previous component. ---@field private _weight number The value used to control of the next button diagonal finding logic strictness. ---@field private _tolerance number Determines how lenient the next button finding logic is. Set larger value for further diagonal navigation. ---@field private _select_trigger hash Select trigger for the current component. Defaults to `druid.const.ACTION_SPACE`. ---@field private _selected_triggers table Table of action_ids that can trigger the selected component. Valid only for the current button when set. ---@field private _selected_component druid.component|druid.button|druid.slider Currently selected button instance. ---@field private _deselect_directions table The valid "escape" direction of the current selection. -local M = component.create("navigation_handler") +local M = {} +-- Components that support navigation. +local COMPONENTS = { "button", "slider" } -M.COMPONENTS = { "button", "slider" } - - ----@private ----@param style druid.navigation_handler.style -function M:on_style_change(style) - self.style = { - on_select = style.on_select or function(_, node, state) end, - } -end ---The constructor for the navigation_handler component. ---@param component druid.component Current druid component that starts as selected. @@ -46,9 +57,11 @@ end function M:init(component, tolerance) -- Set default tolerance if not given. if tolerance == nil then - tolerance = 200 + tolerance = 250 end + pprint(component._component.name) + self._weight = 10 self._tolerance = tolerance self._select_trigger = const.ACTION_SPACE @@ -63,11 +76,9 @@ function M:init(component, tolerance) -- Events self.on_select = event.create() - - -- Set style for the initial component. - self.style.on_select(self, component.node, true) end + ---@private ---@param action_id hash Action id from on_input. ---@param action table Action from on_input. @@ -169,25 +180,28 @@ function M:on_input(action_id, action) return false end + ---Sets a new weight value which affects the next button diagonal finding logic. ---@param new_value number ----@return druid.navigation_handler +---@return druid.widget.navigation_handler self function M:set_weight(new_value) self._weight = new_value return self end + ---Sets a new tolerance value. Can be useful when scale or window size changes. ---@param new_value number How far to allow misalignment on the perpendicular axis when finding the next button. ----@return druid.navigation_handler self The current navigation handler instance. +---@return druid.widget.navigation_handler self The current navigation handler instance. function M:set_tolerance(new_value) self._tolerance = new_value return self end + ---Set input action_id name to trigger selected component by keyboard/gamepad. ---@param key hash|string The action_id of the input key. Example: "key_space". ----@return druid.navigation_handler self The current navigation handler instance. +---@return druid.widget.navigation_handler self The current navigation handler instance. function M:set_select_trigger(key) if type(key) == "string" then self._select_trigger = hash(key) @@ -198,15 +212,17 @@ function M:set_select_trigger(key) return self end + ---Get current the trigger key for currently selected component. ---@return hash _select_trigger The action_id of the input key. function M:get_select_trigger() return self._select_trigger end + ---Set the trigger keys for the selected component. Stays valid until the selected component changes. ---@param keys table|string|hash Supports multiple action_ids if the given value is a table with the action_id hashes or strings. ----@return druid.navigation_handler self The current navigation handler instance. +---@return druid.widget.navigation_handler self The current navigation handler instance. function M:set_temporary_select_triggers(keys) if type(keys) == "table" then for index, value in ipairs(keys) do @@ -224,16 +240,18 @@ function M:set_temporary_select_triggers(keys) return self end + ---Get the currently selected component. ---@return druid.component _selected_component Selected component, which often is a `druid.button`. function M:get_selected_component() return self._selected_component end + ---Set the de-select direction for the selected button. If this is set ---then the next button can only be in that direction. ---@param dir string|table Valid directions: "up", "down", "left", "right". Can take multiple values as a table of strings. ----@return druid.navigation_handler self The current navigation handler instance. +---@return druid.widget.navigation_handler self The current navigation handler instance. function M:set_deselect_directions(dir) if type(dir) == "table" then self._deselect_directions = dir @@ -244,6 +262,7 @@ function M:set_deselect_directions(dir) return self end + ---Returns true if the currently selected `druid.component` is a `druid.button`. ---@private ---@return boolean @@ -251,6 +270,7 @@ function M:_selected_is_button() return self._selected_component._component.name == "button" end + ---Returns true if the currently selected `druid.component` is a `druid.slider`. ---@private ---@return boolean @@ -258,6 +278,7 @@ function M:_selected_is_slider() return self._selected_component._component.name == "slider" end + ---Find the best next button based on the direction from the currently selected button. ---@private ---@param dir string Valid directions: "top", "bottom", "left", "right". @@ -282,7 +303,7 @@ function M:_find_next_button(dir) ---@return boolean local function valid_component(input_component) local component_name = input_component._component.name - for _index, component in ipairs(M.COMPONENTS) do + for _index, component in ipairs(COMPONENTS) do if component_name == component then return true end @@ -346,6 +367,7 @@ function M:_find_next_button(dir) return best_component end + ---De-select the current selected component. ---@private function M:_deselect_current() @@ -361,6 +383,7 @@ function M:_deselect_current() end end + ---Check if the supplied action_id can trigger the selected component. ---@private ---@param action_id hash @@ -375,6 +398,7 @@ function M:_action_id_is_trigger(action_id) return action_id == self._select_trigger end + ---Handle new selection. ---@private ---@param new druid.component Instance of the selected component. @@ -383,9 +407,6 @@ function M:_on_new_select(new) ---@type druid.component local current = self._selected_component - self.style.on_select(self, current.node, false) - self.style.on_select(self, new.node, true) - -- De-select the current component. self:_deselect_current() self._selected_component = new @@ -412,9 +433,10 @@ function M:_on_new_select(new) end --- EVENT - self.on_select:trigger(new, current) + self.on_select:trigger(self:get_context(), new, current) return false end + return M diff --git a/wiki/advanced-setup.md b/wiki/advanced-setup.md index 98828fb..7bf8558 100644 --- a/wiki/advanced-setup.md +++ b/wiki/advanced-setup.md @@ -12,9 +12,12 @@ By default, **Druid** uses all key names from Defold's default `/builtins/input/ - 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: `Space` -> `key_space` (for Navigation Handler widget, optional) - Key trigger: `Esc` -> `key_esc` (for Input component, optional) - Key trigger: `Left` -> `key_left` (for Rich Input component, optional) - Key trigger: `Right` -> `key_right` (for Rich Input component, optional) +- Key trigger: `Up` -> `key_up` (for Navigation Handler widget, optional) +- Key trigger: `Down` -> `key_down` (for Navigation Handler widget, optional) - Key trigger: `Shift` -> `key_lshift` (for Rich Input component, optional) - Key trigger: `Ctrl` -> `key_lctrl` (for Rich Input component, optional) - Key trigger: `Super` -> `key_lsuper` (for Rich Input component, optional) @@ -37,12 +40,15 @@ input_marked_text = marked_text input_key_esc = key_esc input_key_back = key_back input_key_enter = key_enter +input_key_space = key_space input_key_backspace = key_backspace input_multitouch = touch_multi input_scroll_up = mouse_wheel_up input_scroll_down = mouse_wheel_down input_key_left = key_left input_key_right = key_right +input_key_up = key_up +input_key_down = key_down input_key_lshift = key_lshift input_key_lctrl = key_lctrl input_key_lsuper = key_lsuper From 4d58a6d3efab7af879fd1c9ab19b21605b2ed11b Mon Sep 17 00:00:00 2001 From: NaakkaDev Date: Sat, 4 Oct 2025 13:02:34 +0300 Subject: [PATCH 4/5] Removed arguments from the constructor. Cleaned the pprint that somehow sneaked in. The select trigger only takes in hash now. Moved the two lil helper methods out of _find_next_button so they do not get created every time it's called. --- .../navigation_handler/navigation_handler.lua | 107 +++++++++--------- 1 file changed, 56 insertions(+), 51 deletions(-) diff --git a/druid/widget/navigation_handler/navigation_handler.lua b/druid/widget/navigation_handler/navigation_handler.lua index 6fc40f4..f2a3eb8 100644 --- a/druid/widget/navigation_handler/navigation_handler.lua +++ b/druid/widget/navigation_handler/navigation_handler.lua @@ -17,7 +17,10 @@ local const = require("druid.const") ---`local navigation_handler = require("druid.widgets.navigation_handler.navigation_handler")` --- ---Create the new widget instance: ----`self.nav = self.druid:new_widget(navigation_handler, self.my_button)` +---`self.nav = self.druid:new_widget(navigation_handler)` +--- +---Set the first component instance (likely a button) to be selected. This is **required**. +---`self.nav:select_component(self.my_button)` --- --- ---### Example using the `on_select` event @@ -51,29 +54,43 @@ local M = {} local COMPONENTS = { "button", "slider" } ----The constructor for the navigation_handler component. ----@param component druid.component Current druid component that starts as selected. ----@param tolerance number|nil How far to allow misalignment on the perpendicular axis when finding the next component. -function M:init(component, tolerance) - -- Set default tolerance if not given. - if tolerance == nil then - tolerance = 250 +---Helper method for checking if the given direction is valid. +---@param dirs table +---@param dir string +---@return boolean +local function valid_direction(dirs, dir) + for _index, value in ipairs(dirs) do + if value == dir then + return true + end end + return false +end - pprint(component._component.name) +---Helper method for checking iterating through components. +---Returns true if the given component is in the table of valid components. +---@param input_component druid.component +---@return boolean +local function valid_component(input_component) + local component_name = input_component._component.name + for _index, component in ipairs(COMPONENTS) do + if component_name == component then + return true + end + end + return false +end + +---The constructor for the navigation_handler widget. +function M:init() self._weight = 10 - self._tolerance = tolerance + self._tolerance = 250 self._select_trigger = const.ACTION_SPACE self._selected_triggers = {} - self._selected_component = component + self._selected_component = nil self._deselect_directions = {} - -- Select the component if it's a button. - if component.hover then - component.hover:set_hover(true) - end - -- Events self.on_select = event.create() end @@ -84,6 +101,11 @@ end ---@param action table Action from on_input. ---@return boolean is_consumed True if the input was consumed. function M:on_input(action_id, action) + -- Do nothing if the selected component is not set. + if not self._selected_component then + return + end + -- Trigger an action with the selected component, e.g. button click. if self:_action_id_is_trigger(action_id) and self:_selected_is_button() then ---@type druid.button @@ -181,6 +203,21 @@ function M:on_input(action_id, action) end +---Set the given `druid.component` as selected component. +---@param component druid.component Current druid component that starts as selected. +---@return druid.widget.navigation_handler self +function M:select_component(component) + self._selected_component = component + + -- Select the component if it's a button. + if component.hover then + component.hover:set_hover(true) + end + + return self +end + + ---Sets a new weight value which affects the next button diagonal finding logic. ---@param new_value number ---@return druid.widget.navigation_handler self @@ -200,15 +237,10 @@ end ---Set input action_id name to trigger selected component by keyboard/gamepad. ----@param key hash|string The action_id of the input key. Example: "key_space". +---@param key hash The action_id of the input key. Example: "key_space". ---@return druid.widget.navigation_handler self The current navigation handler instance. function M:set_select_trigger(key) - if type(key) == "string" then - self._select_trigger = hash(key) - else - self._select_trigger = key - end - + self._select_trigger = key return self end @@ -242,7 +274,7 @@ end ---Get the currently selected component. ----@return druid.component _selected_component Selected component, which often is a `druid.button`. +---@return druid.component|nil _selected_component Selected component, which often is a `druid.button`. function M:get_selected_component() return self._selected_component end @@ -284,33 +316,6 @@ end ---@param dir string Valid directions: "top", "bottom", "left", "right". ---@return druid.component|nil function M:_find_next_button(dir) - ---Helper method for checking if the given direction is valid. - ---@param dirs table - ---@param dir string - ---@return boolean - local function valid_direction(dirs, dir) - for _index, value in ipairs(dirs) do - if value == dir then - return true - end - end - return false - end - - ---Helper method for checking iterating through components. - ---Returns true if the given component is in the table of valid components. - ---@param input_component druid.component - ---@return boolean - local function valid_component(input_component) - local component_name = input_component._component.name - for _index, component in ipairs(COMPONENTS) do - if component_name == component then - return true - end - end - return false - end - -- Check if the deselect direction is set and -- the direction is different from it. if next(self._deselect_directions) ~= nil and not valid_direction(self._deselect_directions, dir) then From a5e4b95f7bcbb400a8f8d2a70f82942dc2559afd Mon Sep 17 00:00:00 2001 From: NaakkaDev Date: Sat, 4 Oct 2025 14:51:48 +0300 Subject: [PATCH 5/5] Moved the two lil helper methods down and adopted them into the model space :P --- .../navigation_handler/navigation_handler.lua | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/druid/widget/navigation_handler/navigation_handler.lua b/druid/widget/navigation_handler/navigation_handler.lua index f2a3eb8..76b64b3 100644 --- a/druid/widget/navigation_handler/navigation_handler.lua +++ b/druid/widget/navigation_handler/navigation_handler.lua @@ -54,34 +54,6 @@ local M = {} local COMPONENTS = { "button", "slider" } ----Helper method for checking if the given direction is valid. ----@param dirs table ----@param dir string ----@return boolean -local function valid_direction(dirs, dir) - for _index, value in ipairs(dirs) do - if value == dir then - return true - end - end - return false -end - ----Helper method for checking iterating through components. ----Returns true if the given component is in the table of valid components. ----@param input_component druid.component ----@return boolean -local function valid_component(input_component) - local component_name = input_component._component.name - for _index, component in ipairs(COMPONENTS) do - if component_name == component then - return true - end - end - return false -end - - ---The constructor for the navigation_handler widget. function M:init() self._weight = 10 @@ -318,7 +290,7 @@ end function M:_find_next_button(dir) -- Check if the deselect direction is set and -- the direction is different from it. - if next(self._deselect_directions) ~= nil and not valid_direction(self._deselect_directions, dir) then + if next(self._deselect_directions) ~= nil and not M._valid_direction(self._deselect_directions, dir) then return nil end @@ -342,7 +314,7 @@ function M:_find_next_button(dir) end -- Only check components that are supported. - if input_component ~= self._selected_component and valid_component(input_component) then + if input_component ~= self._selected_component and M._valid_component(input_component) then local pos = gui.get_screen_position(node) local dx, dy = pos.x - screen_pos.x, pos.y - screen_pos.y local valid = false @@ -444,4 +416,35 @@ function M:_on_new_select(new) end +---Helper method for checking if the given direction is valid. +---@private +---@param dirs table +---@param dir string +---@return boolean +function M._valid_direction(dirs, dir) + for _index, value in ipairs(dirs) do + if value == dir then + return true + end + end + return false +end + + +---Helper method for checking iterating through components. +---Returns true if the given component is in the table of valid components. +---@private +---@param input_component druid.component +---@return boolean +function M._valid_component(input_component) + local component_name = input_component._component.name + for _index, component in ipairs(COMPONENTS) do + if component_name == component then + return true + end + end + return false +end + + return M